From 685d931f8f00b433e271feeab53cb9edf3c9f6d6 Mon Sep 17 00:00:00 2001 From: sowens-csd Date: Thu, 3 Oct 2024 11:40:09 -0400 Subject: [PATCH] feat: support for custom phrase aggregator --- .../darwin/Classes/SpeechToTextPlugin.swift | 8 +- speech_to_text/example/lib/main.dart | 7 +- speech_to_text/example/pubspec.lock | 18 ++-- speech_to_text/lib/balanced_alternates.dart | 6 +- .../lib/speech_recognition_error.g.dart | 11 +-- .../lib/speech_recognition_result.dart | 15 ++- .../lib/speech_recognition_result.g.dart | 31 +++--- speech_to_text/lib/speech_to_text.dart | 39 ++++++++ speech_to_text/pubspec.lock | 99 +++++++++++-------- speech_to_text/pubspec.yaml | 2 +- .../test/speech_recognition_result_test.dart | 4 +- .../test/speech_recognitions_words_test.dart | 22 ++++- speech_to_text/test/speech_to_text_test.dart | 34 +++++++ .../test/test_speech_channel_handler.dart | 12 ++- .../test/test_speech_to_text_platform.dart | 4 +- 15 files changed, 225 insertions(+), 87 deletions(-) diff --git a/speech_to_text/darwin/Classes/SpeechToTextPlugin.swift b/speech_to_text/darwin/Classes/SpeechToTextPlugin.swift index 90fa84c3..136ca147 100644 --- a/speech_to_text/darwin/Classes/SpeechToTextPlugin.swift +++ b/speech_to_text/darwin/Classes/SpeechToTextPlugin.swift @@ -53,6 +53,7 @@ public enum ListenMode: Int { struct SpeechRecognitionWords: Codable { let recognizedWords: String + let recognizedPhrases: [String]? let confidence: Decimal } @@ -889,8 +890,10 @@ private class SpeechResultAggregator { if hasPreviousTranscriptions { var lowestConfidence: Decimal = 1.0 var aggregatePhrase = "" + var recognizedPhrases: [String] = [] for previousTranscription in previousTranscriptions { if let transcription = previousTranscription.first { + recognizedPhrases.append(transcription.formattedString) lowestConfidence = min( lowestConfidence, confidenceIn(transcription)) if aggregatePhrase.count > 0 && aggregatePhrase.last != " " { aggregatePhrase += " " @@ -899,17 +902,18 @@ private class SpeechResultAggregator { } } if let transcription = speechTranscriptions.first { + recognizedPhrases.append(transcription.formattedString) lowestConfidence = min( lowestConfidence, confidenceIn(transcription)) if aggregatePhrase.count > 0 && aggregatePhrase.last != " " { aggregatePhrase += " " } aggregatePhrase += transcription.formattedString } - speechWords.append(SpeechRecognitionWords(recognizedWords: aggregatePhrase, confidence: lowestConfidence)) + speechWords.append(SpeechRecognitionWords(recognizedWords: aggregatePhrase, recognizedPhrases: recognizedPhrases, confidence: lowestConfidence)) } for transcription in speechTranscriptions { let words: SpeechRecognitionWords = SpeechRecognitionWords( - recognizedWords: transcription.formattedString, confidence: confidenceIn(transcription)) + recognizedWords: transcription.formattedString, recognizedPhrases: nil, confidence: confidenceIn(transcription)) speechWords.append(words) } return SpeechRecognitionResult(alternates: speechWords, finalResult: isFinal ) diff --git a/speech_to_text/example/lib/main.dart b/speech_to_text/example/lib/main.dart index 2cdec733..f84a4a9a 100644 --- a/speech_to_text/example/lib/main.dart +++ b/speech_to_text/example/lib/main.dart @@ -54,6 +54,7 @@ class _SpeechSampleAppState extends State { debugLogging: _logEvents, ); if (hasSpeech) { + speech.unexpectedPhraseAggregator = _punctAggregator; // Get the list of languages installed on the supporting platform so they // can be displayed in the UI for selection by the user. _localeNames = await speech.locales(); @@ -74,6 +75,10 @@ class _SpeechSampleAppState extends State { } } + String _punctAggregator(List phrases) { + return phrases.join('. '); + } + @override Widget build(BuildContext context) { return MaterialApp( @@ -128,7 +133,7 @@ class _SpeechSampleAppState extends State { listenMode: ListenMode.confirmation, cancelOnError: true, partialResults: true, - autoPunctuation: true, + autoPunctuation: false, enableHapticFeedback: true); // Note that `listenFor` is the maximum, not the minimum, on some // systems recognition will be stopped before this value is reached. diff --git a/speech_to_text/example/pubspec.lock b/speech_to_text/example/pubspec.lock index fc2177ba..113fb6a8 100644 --- a/speech_to_text/example/pubspec.lock +++ b/speech_to_text/example/pubspec.lock @@ -58,10 +58,10 @@ packages: dependency: "direct dev" description: name: flutter_lints - sha256: e2a421b7e59244faef694ba7b30562e489c2b489866e505074eb005cd7060db7 + sha256: "9e8c3858111da373efc5aa341de011d9bd23e2c5c5e0c62bccf32438e192d7b1" url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.0.2" flutter_test: dependency: "direct dev" description: flutter @@ -76,10 +76,10 @@ packages: dependency: transitive description: name: json_annotation - sha256: b10a7b2ff83d83c777edba3c6a0f97045ddadd56c944e1a23a3fdf43a1bf4467 + sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1" url: "https://pub.dev" source: hosted - version: "4.8.1" + version: "4.9.0" leak_tracker: dependency: transitive description: @@ -172,10 +172,10 @@ packages: dependency: "direct main" description: name: provider - sha256: "9a96a0a19b594dbc5bf0f1f27d2bc67d5f95957359b461cd9feb44ed6ae75096" + sha256: c8a055ee5ce3fd98d6fc872478b03823ffdb448699c6ebdbbc71d59b596fd48c url: "https://pub.dev" source: hosted - version: "6.1.1" + version: "6.1.2" sky_engine: dependency: transitive description: flutter @@ -195,7 +195,7 @@ packages: path: ".." relative: true source: path - version: "7.0.0" + version: "7.1.0-beta.1" speech_to_text_platform_interface: dependency: transitive description: @@ -264,10 +264,10 @@ packages: dependency: transitive description: name: web - sha256: d43c1d6b787bf0afad444700ae7f4db8827f701bc61c255ac8d328c6f4d52062 + sha256: cd3543bd5798f6ad290ea73d210f423502e71900302dde696f8bff84bf89a1cb url: "https://pub.dev" source: hosted - version: "1.0.0" + version: "1.1.0" sdks: dart: ">=3.4.0 <4.0.0" flutter: ">=3.18.0-18.0.pre.54" diff --git a/speech_to_text/lib/balanced_alternates.dart b/speech_to_text/lib/balanced_alternates.dart index 11a2dbc3..c4c13df6 100644 --- a/speech_to_text/lib/balanced_alternates.dart +++ b/speech_to_text/lib/balanced_alternates.dart @@ -24,7 +24,7 @@ class BalancedAlternates { /// void add(int phrase, String words, double confidence) { _alternates[phrase] ??= []; - _alternates[phrase]?.add(SpeechRecognitionWords(words, confidence)); + _alternates[phrase]?.add(SpeechRecognitionWords(words, null, confidence)); } /// Return the full speech recognition results which is the concatenation @@ -62,8 +62,8 @@ class BalancedAlternates { alternateConfidence = min(alternateConfidence, _alternates[phraseIndex]![altCount].confidence); } - result - .add(SpeechRecognitionWords(alternatePhrase, alternateConfidence)); + result.add( + SpeechRecognitionWords(alternatePhrase, null, alternateConfidence)); } } else { for (var phraseIndex = phraseCount - 1; phraseIndex >= 0; --phraseIndex) { diff --git a/speech_to_text/lib/speech_recognition_error.g.dart b/speech_to_text/lib/speech_recognition_error.g.dart index 65299f6d..1f062604 100644 --- a/speech_to_text/lib/speech_recognition_error.g.dart +++ b/speech_to_text/lib/speech_recognition_error.g.dart @@ -7,12 +7,11 @@ part of 'speech_recognition_error.dart'; // ************************************************************************** SpeechRecognitionError _$SpeechRecognitionErrorFromJson( - Map json) { - return SpeechRecognitionError( - json['errorMsg'] as String, - json['permanent'] as bool, - ); -} + Map json) => + SpeechRecognitionError( + json['errorMsg'] as String, + json['permanent'] as bool, + ); Map _$SpeechRecognitionErrorToJson( SpeechRecognitionError instance) => diff --git a/speech_to_text/lib/speech_recognition_result.dart b/speech_to_text/lib/speech_recognition_result.dart index fae15326..f79d4c9f 100644 --- a/speech_to_text/lib/speech_recognition_result.dart +++ b/speech_to_text/lib/speech_recognition_result.dart @@ -96,6 +96,18 @@ class SpeechRecognitionWords { /// The sequence of words recognized final String recognizedWords; + /// If the platform provides it, a list of phrases that were recognized + /// as individual utterances. This can generally be ignored as it + /// is usually null and where it is not [recognizedWords] will contain + /// the same information aggregated into a single string. + /// Currently this is only populated on iOS 17.5 and 18 where a bug in + /// the speech recognizer causes unexpected extra phrases. These are + /// automatically handled by the plugin and recognizedWords will be + /// an aggregate of all the phrases. To customize the handling of + /// these phrases, use the [SpeechToText.unexpectedPhraseAggregator] property + /// to customize the aggregation. + final List? recognizedPhrases; + /// The confidence that the [recognizedWords] are correct. /// /// Confidence is expressed as a value between 0 and 1. 0 @@ -106,7 +118,8 @@ class SpeechRecognitionWords { static const double confidenceThreshold = 0.8; static const double missingConfidence = -1; - const SpeechRecognitionWords(this.recognizedWords, this.confidence); + const SpeechRecognitionWords( + this.recognizedWords, this.recognizedPhrases, this.confidence); /// true if there is confidence in this recognition, false otherwise. /// diff --git a/speech_to_text/lib/speech_recognition_result.g.dart b/speech_to_text/lib/speech_recognition_result.g.dart index b5738b48..778315d2 100644 --- a/speech_to_text/lib/speech_recognition_result.g.dart +++ b/speech_to_text/lib/speech_recognition_result.g.dart @@ -7,14 +7,14 @@ part of 'speech_recognition_result.dart'; // ************************************************************************** SpeechRecognitionResult _$SpeechRecognitionResultFromJson( - Map json) { - return SpeechRecognitionResult( - (json['alternates'] as List) - .map((e) => SpeechRecognitionWords.fromJson(e as Map)) - .toList(), - json['finalResult'] as bool, - ); -} + Map json) => + SpeechRecognitionResult( + (json['alternates'] as List) + .map( + (e) => SpeechRecognitionWords.fromJson(e as Map)) + .toList(), + json['finalResult'] as bool, + ); Map _$SpeechRecognitionResultToJson( SpeechRecognitionResult instance) => @@ -24,16 +24,19 @@ Map _$SpeechRecognitionResultToJson( }; SpeechRecognitionWords _$SpeechRecognitionWordsFromJson( - Map json) { - return SpeechRecognitionWords( - json['recognizedWords'] as String, - (json['confidence'] as num).toDouble(), - ); -} + Map json) => + SpeechRecognitionWords( + json['recognizedWords'] as String, + (json['recognizedPhrases'] as List?) + ?.map((e) => e as String) + .toList(), + (json['confidence'] as num).toDouble(), + ); Map _$SpeechRecognitionWordsToJson( SpeechRecognitionWords instance) => { 'recognizedWords': instance.recognizedWords, + 'recognizedPhrases': instance.recognizedPhrases, 'confidence': instance.confidence, }; diff --git a/speech_to_text/lib/speech_to_text.dart b/speech_to_text/lib/speech_to_text.dart index 24cf9301..b2841922 100644 --- a/speech_to_text/lib/speech_to_text.dart +++ b/speech_to_text/lib/speech_to_text.dart @@ -71,6 +71,14 @@ typedef SpeechErrorListener = void Function( /// See the [onStatus] argument on the [SpeechToText.initialize] method for use. typedef SpeechStatusListener = void Function(String status); +/// Aggregates multiple phrases into a single result. This is used when +/// the platform returns multiple phrases for a single utterance. The default +/// behaviour is to concatenate the phrases into a single result with spaces +/// separating the phrases and no change to capitalization. This can +/// be overridden to provide a different aggregation strategy. +/// see [_defaultPhraseAggregator] for the default implementation. +typedef SpeechPhraseAggregator = String Function(List phrases); + /// Notified when the sound level changes during a listen method. /// /// [level] is a measure of the decibels of the current sound on @@ -196,6 +204,13 @@ class SpeechToText { SpeechStatusListener? statusListener; SpeechSoundLevelChange? _soundLevelChange; + /// This overrides the default phrase aggregator to allow for + /// different strategies for aggregating multiple phrases into + /// a single result. This is used when the platform unexpectedly + /// returns multiple phrases for a single utterance. Currently + /// this happens only due to a bug in iOS 17.5/18 + SpeechPhraseAggregator? unexpectedPhraseAggregator; + factory SpeechToText() => _instance; @visibleForTesting @@ -615,9 +630,29 @@ class SpeechToText { // print('onTextRecognition'); Map resultMap = jsonDecode(resultJson); var speechResult = SpeechRecognitionResult.fromJson(resultMap); + speechResult = _checkAggregates(speechResult); _notifyResults(speechResult); } + /// Checks the result for multiple phrases and aggregates them if needed. + /// Returns a new result with the aggregated phrases. + SpeechRecognitionResult _checkAggregates(SpeechRecognitionResult result) { + var alternates = []; + for (var alternate in result.alternates) { + if (alternate.recognizedPhrases != null) { + final aggregated = (unexpectedPhraseAggregator ?? + _defaultPhraseAggregator)(alternate.recognizedPhrases!); + final aggregatedWords = SpeechRecognitionWords( + aggregated, alternate.recognizedPhrases, alternate.confidence); + alternates.add(aggregatedWords); + } else { + alternates.add(alternate); + } + // print(' ${alternate.recognizedWords} ${alternate.confidence}'); + } + return SpeechRecognitionResult(alternates, result.finalResult); + } + void _onFinalTimeout() { // print('onFinalTimeout $_finalTimeout'); if (_notifiedFinal) return; @@ -715,6 +750,10 @@ class SpeechToText { _notifyFinalTimer = null; _listenTimer = null; } + + String _defaultPhraseAggregator(List phrases) { + return phrases.join(' '); + } } /// Thrown when a method is called that requires successful diff --git a/speech_to_text/pubspec.lock b/speech_to_text/pubspec.lock index 21110b7d..4955d14f 100644 --- a/speech_to_text/pubspec.lock +++ b/speech_to_text/pubspec.lock @@ -5,26 +5,31 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: eb376e9acf6938204f90eb3b1f00b578640d3188b4c8a8ec054f9f479af8d051 + sha256: f256b0c0ba6c7577c15e2e4e114755640a875e885099367bf6e012b19314c834 url: "https://pub.dev" source: hosted - version: "64.0.0" + version: "72.0.0" + _macros: + dependency: transitive + description: dart + source: sdk + version: "0.3.2" analyzer: dependency: transitive description: name: analyzer - sha256: "69f54f967773f6c26c7dcb13e93d7ccee8b17a641689da39e878d5cf13b06893" + sha256: b652861553cd3990d8ed361f7979dc6d7053a9ac8843fa73820ab68ce5410139 url: "https://pub.dev" source: hosted - version: "6.2.0" + version: "6.7.0" args: dependency: transitive description: name: args - sha256: eef6c46b622e0494a36c5a12d10d77fb4e855501a91c1b9ef9339326e58f0596 + sha256: "7cf60b9f0cc88203c5a190b4cd62a99feea42759a7fa695010eb5de1c0b2252a" url: "https://pub.dev" source: hosted - version: "2.4.2" + version: "2.5.0" async: dependency: transitive description: @@ -61,10 +66,10 @@ packages: dependency: transitive description: name: build_daemon - sha256: "0343061a33da9c5810b2d6cee51945127d8f4c060b7fbdd9d54917f0a3feaaa1" + sha256: "79b2aef6ac2ed00046867ed354c88778c9c0f029df8a20fe10b5436826721ef9" url: "https://pub.dev" source: hosted - version: "4.0.1" + version: "4.0.2" build_resolvers: dependency: transitive description: @@ -77,18 +82,18 @@ packages: dependency: "direct dev" description: name: build_runner - sha256: "581bacf68f89ec8792f5e5a0b2c4decd1c948e97ce659dc783688c8a88fbec21" + sha256: "028819cfb90051c6b5440c7e574d1896f8037e3c96cf17aaeb054c9311cfbf4d" url: "https://pub.dev" source: hosted - version: "2.4.8" + version: "2.4.13" build_runner_core: dependency: transitive description: name: build_runner_core - sha256: c9e32d21dd6626b5c163d48b037ce906bbe428bc23ab77bcd77bb21e593b6185 + sha256: f8126682b87a7282a339b871298cc12009cb67109cfa1614d6436fb0289193e0 url: "https://pub.dev" source: hosted - version: "7.2.11" + version: "7.3.2" built_collection: dependency: transitive description: @@ -101,10 +106,10 @@ packages: dependency: transitive description: name: built_value - sha256: a3ec2e0f967bc47f69f95009bb93db936288d61d5343b9436e378b28a2f830c6 + sha256: c7913a9737ee4007efedaffc968c049fd0f3d0e49109e778edc10de9426005cb url: "https://pub.dev" source: hosted - version: "8.9.0" + version: "8.9.2" characters: dependency: transitive description: @@ -157,18 +162,18 @@ packages: dependency: transitive description: name: crypto - sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab + sha256: ec30d999af904f33454ba22ed9a86162b35e52b44ac4807d1d93c288041d7d27 url: "https://pub.dev" source: hosted - version: "3.0.3" + version: "3.0.5" dart_style: dependency: transitive description: name: dart_style - sha256: "40ae61a5d43feea6d24bd22c0537a6629db858963b99b4bc1c3db80676f32368" + sha256: "7856d364b589d1f08986e140938578ed36ed948581fbc3bc9aef1805039ac5ab" url: "https://pub.dev" source: hosted - version: "2.3.4" + version: "2.3.7" fake_async: dependency: "direct dev" description: @@ -202,10 +207,10 @@ packages: dependency: "direct dev" description: name: flutter_lints - sha256: e2a421b7e59244faef694ba7b30562e489c2b489866e505074eb005cd7060db7 + sha256: "9e8c3858111da373efc5aa341de011d9bd23e2c5c5e0c62bccf32438e192d7b1" url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.0.2" flutter_test: dependency: "direct dev" description: flutter @@ -220,10 +225,10 @@ packages: dependency: transitive description: name: frontend_server_client - sha256: "408e3ca148b31c20282ad6f37ebfa6f4bdc8fede5b74bc2f08d9d92b55db3612" + sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694 url: "https://pub.dev" source: hosted - version: "3.2.0" + version: "4.0.0" glob: dependency: transitive description: @@ -236,10 +241,10 @@ packages: dependency: transitive description: name: graphs - sha256: aedc5a15e78fc65a6e23bcd927f24c64dd995062bcd1ca6eda65a3cff92a4d19 + sha256: "741bbf84165310a68ff28fe9e727332eef1407342fca52759cb21ad8177bb8d0" url: "https://pub.dev" source: hosted - version: "2.3.1" + version: "2.3.2" http_multi_server: dependency: transitive description: @@ -276,18 +281,18 @@ packages: dependency: "direct main" description: name: json_annotation - sha256: b10a7b2ff83d83c777edba3c6a0f97045ddadd56c944e1a23a3fdf43a1bf4467 + sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1" url: "https://pub.dev" source: hosted - version: "4.8.1" + version: "4.9.0" json_serializable: dependency: "direct dev" description: name: json_serializable - sha256: aa1f5a8912615733e0fdc7a02af03308933c93235bdc8d50d0b0c8a8ccb0b969 + sha256: ea1432d167339ea9b5bb153f0571d0039607a873d6e04e0117af043f14a1fd4b url: "https://pub.dev" source: hosted - version: "6.7.1" + version: "6.8.0" leak_tracker: dependency: transitive description: @@ -328,6 +333,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.0" + macros: + dependency: transitive + description: + name: macros + sha256: "0acaed5d6b7eab89f63350bccd82119e6c602df0f391260d0e32b5e23db79536" + url: "https://pub.dev" + source: hosted + version: "0.1.2-main.4" matcher: dependency: transitive description: @@ -356,10 +369,10 @@ packages: dependency: transitive description: name: mime - sha256: "2e123074287cc9fd6c09de8336dae606d1ddb88d9ac47358826db698c176a1f2" + sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" url: "https://pub.dev" source: hosted - version: "1.0.5" + version: "2.0.0" mockito: dependency: "direct dev" description: @@ -420,10 +433,10 @@ packages: dependency: transitive description: name: pubspec_parse - sha256: c63b2876e58e194e4b0828fcb080ad0e06d051cb607a6be51a9e084f47cb9367 + sha256: c799b721d79eb6ee6fa56f00c04b472dcd44a30d258fac2174a6ec57302678f8 url: "https://pub.dev" source: hosted - version: "1.2.3" + version: "1.3.0" shelf: dependency: transitive description: @@ -436,10 +449,10 @@ packages: dependency: transitive description: name: shelf_web_socket - sha256: "9ca081be41c60190ebcb4766b2486a7d50261db7bd0f5d9615f2d653637a84c1" + sha256: "073c147238594ecd0d193f3456a5fe91c4b0abbcc68bf5cd95b36c4e194ac611" url: "https://pub.dev" source: hosted - version: "1.0.4" + version: "2.0.0" sky_engine: dependency: transitive description: flutter @@ -569,18 +582,26 @@ packages: dependency: "direct main" description: name: web - sha256: d43c1d6b787bf0afad444700ae7f4db8827f701bc61c255ac8d328c6f4d52062 + sha256: cd3543bd5798f6ad290ea73d210f423502e71900302dde696f8bff84bf89a1cb url: "https://pub.dev" source: hosted - version: "1.0.0" + version: "1.1.0" + web_socket: + dependency: transitive + description: + name: web_socket + sha256: "3c12d96c0c9a4eec095246debcea7b86c0324f22df69893d538fcc6f1b8cce83" + url: "https://pub.dev" + source: hosted + version: "0.1.6" web_socket_channel: dependency: transitive description: name: web_socket_channel - sha256: d88238e5eac9a42bb43ca4e721edba3c08c6354d4a53063afaa568516217621b + sha256: "9f187088ed104edd8662ca07af4b124465893caf063ba29758f97af57e61da8f" url: "https://pub.dev" source: hosted - version: "2.4.0" + version: "3.0.1" yaml: dependency: transitive description: @@ -590,5 +611,5 @@ packages: source: hosted version: "3.1.2" sdks: - dart: ">=3.4.0 <4.0.0" + dart: ">=3.5.0 <4.0.0" flutter: ">=3.18.0-18.0.pre.54" diff --git a/speech_to_text/pubspec.yaml b/speech_to_text/pubspec.yaml index 30a1162f..c12298e0 100644 --- a/speech_to_text/pubspec.yaml +++ b/speech_to_text/pubspec.yaml @@ -1,6 +1,6 @@ name: speech_to_text description: A Flutter plugin that exposes device specific speech to text recognition capability. -version: 7.1.0-beta.1 +version: 7.1.0-beta.2 homepage: https://github.com/csdcorp/speech_to_text environment: diff --git a/speech_to_text/test/speech_recognition_result_test.dart b/speech_to_text/test/speech_recognition_result_test.dart index 08d1b006..19828f3d 100644 --- a/speech_to_text/test/speech_recognition_result_test.dart +++ b/speech_to_text/test/speech_recognition_result_test.dart @@ -11,9 +11,9 @@ void main() { final firstRecognizedJson = '{"alternates":[{"recognizedWords":"$firstRecognizedWords","confidence":$firstConfidence}],"finalResult":false}'; final firstWords = - SpeechRecognitionWords(firstRecognizedWords, firstConfidence); + SpeechRecognitionWords(firstRecognizedWords, null, firstConfidence); final secondWords = - SpeechRecognitionWords(secondRecognizedWords, secondConfidence); + SpeechRecognitionWords(secondRecognizedWords, null, secondConfidence); setUp(() {}); diff --git a/speech_to_text/test/speech_recognitions_words_test.dart b/speech_to_text/test/speech_recognitions_words_test.dart index e3df3365..40d5460e 100644 --- a/speech_to_text/test/speech_recognitions_words_test.dart +++ b/speech_to_text/test/speech_recognitions_words_test.dart @@ -11,9 +11,9 @@ void main() { final firstRecognizedJson = '{"recognizedWords":"$firstRecognizedWords","confidence":$firstConfidence}'; final firstWords = - SpeechRecognitionWords(firstRecognizedWords, firstConfidence); + SpeechRecognitionWords(firstRecognizedWords, null, firstConfidence); final secondWords = - SpeechRecognitionWords(secondRecognizedWords, secondConfidence); + SpeechRecognitionWords(secondRecognizedWords, null, secondConfidence); setUp(() {}); @@ -32,7 +32,7 @@ void main() { }); test('equals true for different object with same values', () { var firstWordsA = - SpeechRecognitionWords(firstRecognizedWords, firstConfidence); + SpeechRecognitionWords(firstRecognizedWords, null, firstConfidence); expect(firstWords, firstWordsA); }); test('equals false for different results', () { @@ -43,7 +43,7 @@ void main() { }); test('hash same for different object with same values', () { var firstWordsA = - SpeechRecognitionWords(firstRecognizedWords, firstConfidence); + SpeechRecognitionWords(firstRecognizedWords, null, firstConfidence); expect(firstWords.hashCode, firstWordsA.hashCode); }); test('hash different for different results', () { @@ -62,7 +62,7 @@ void main() { }); test('true when missing', () { var words = SpeechRecognitionWords( - firstRecognizedWords, SpeechRecognitionWords.missingConfidence); + firstRecognizedWords, null, SpeechRecognitionWords.missingConfidence); expect(words.isConfident(), isTrue); expect(words.hasConfidenceRating, isFalse); }); @@ -81,5 +81,17 @@ void main() { var roundtripWords = SpeechRecognitionWords.fromJson(roundTripJson); expect(words, roundtripWords); }); + test('roundtrips correctly with phrases', () { + final phrase1 = 'first part'; + final phrase2 = 'second part'; + final initialWords = SpeechRecognitionWords( + firstRecognizedWords, [phrase1, phrase2], firstConfidence); + var roundTripJson = initialWords.toJson(); + var roundtripWords = SpeechRecognitionWords.fromJson(roundTripJson); + expect(roundtripWords.recognizedPhrases, + containsAll(initialWords.recognizedPhrases!)); + expect(roundtripWords.recognizedWords, initialWords.recognizedWords); + expect(roundtripWords.confidence, initialWords.confidence); + }); }); } diff --git a/speech_to_text/test/speech_to_text_test.dart b/speech_to_text/test/speech_to_text_test.dart index 90cd92ad..105d3a7b 100644 --- a/speech_to_text/test/speech_to_text_test.dart +++ b/speech_to_text/test/speech_to_text_test.dart @@ -1,3 +1,5 @@ +import 'dart:convert'; + import 'package:fake_async/fake_async.dart'; import 'package:mockito/mockito.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -363,6 +365,38 @@ void main() { expect(speech.lastRecognizedWords, TestSpeechChannelHandler.secondRecognizedWords); }); + test('aggregates phrases if provided', () async { + await speech.initialize(); + await speech.listen(onResult: listener.onSpeechResult); + final resultWithAggregaes = SpeechRecognitionResult([ + TestSpeechChannelHandler.firstPhrases, + ], false); + testPlatform.onTextRecognition!(jsonEncode(resultWithAggregaes.toJson())); + expect(listener.speechResults, 1); + expect( + listener.results.first.recognizedWords, + TestSpeechChannelHandler.firstAggregatePhrases, + ); + expect(speech.lastRecognizedWords, + TestSpeechChannelHandler.firstAggregatePhrases); + }); + test('uses custom aggregator if provided', () async { + await speech.initialize(); + speech.unexpectedPhraseAggregator = (phrases) => phrases.join('. '); + await speech.listen(onResult: listener.onSpeechResult); + final resultWithAggregaes = SpeechRecognitionResult([ + TestSpeechChannelHandler.firstPhrases, + ], false); + testPlatform.onTextRecognition!(jsonEncode(resultWithAggregaes.toJson())); + expect(listener.speechResults, 1); + final expectedAggregate = + '${TestSpeechChannelHandler.firstRecognizedWords}. ${TestSpeechChannelHandler.secondRecognizedWords}'; + expect( + listener.results.first.recognizedWords, + expectedAggregate, + ); + expect(speech.lastRecognizedWords, expectedAggregate); + }); }); group('status callback', () { diff --git a/speech_to_text/test/test_speech_channel_handler.dart b/speech_to_text/test/test_speech_channel_handler.dart index 1aa945cc..33d9dff3 100644 --- a/speech_to_text/test/test_speech_channel_handler.dart +++ b/speech_to_text/test/test_speech_channel_handler.dart @@ -45,15 +45,23 @@ class TestSpeechChannelHandler { static const String finalRecognizedJson = '{"alternates":[{"recognizedWords":"$secondRecognizedWords","confidence":$secondConfidence}],"finalResult":true}'; static const SpeechRecognitionWords firstWords = - SpeechRecognitionWords(firstRecognizedWords, firstConfidence); + SpeechRecognitionWords(firstRecognizedWords, null, firstConfidence); static const SpeechRecognitionWords secondWords = - SpeechRecognitionWords(secondRecognizedWords, secondConfidence); + SpeechRecognitionWords(secondRecognizedWords, null, secondConfidence); + static const SpeechRecognitionWords firstPhrases = SpeechRecognitionWords( + secondRecognizedWords, + [firstRecognizedWords, secondRecognizedWords], + secondConfidence); + static const firstAggregatePhrases = + '$firstRecognizedWords $secondRecognizedWords'; static final SpeechRecognitionResult firstRecognizedResult = SpeechRecognitionResult([firstWords], false); static final SpeechRecognitionResult secondRecognizedResult = SpeechRecognitionResult([secondWords], false); static final SpeechRecognitionResult finalRecognizedResult = SpeechRecognitionResult([secondWords], true); + static final SpeechRecognitionResult firstPhrasesResult = + SpeechRecognitionResult([firstPhrases], true); static const String transientErrorJson = '{"errorMsg":"network","permanent":false}'; static const String permanentErrorJson = diff --git a/speech_to_text/test/test_speech_to_text_platform.dart b/speech_to_text/test/test_speech_to_text_platform.dart index b9933161..f82a02d1 100644 --- a/speech_to_text/test/test_speech_to_text_platform.dart +++ b/speech_to_text/test/test_speech_to_text_platform.dart @@ -37,9 +37,9 @@ class TestSpeechToTextPlatform extends SpeechToTextPlatform { static const String finalRecognizedJson = '{"alternates":[{"recognizedWords":"$secondRecognizedWords","confidence":$secondConfidence}],"finalResult":true}'; static const SpeechRecognitionWords firstWords = - SpeechRecognitionWords(firstRecognizedWords, firstConfidence); + SpeechRecognitionWords(firstRecognizedWords, null, firstConfidence); static const SpeechRecognitionWords secondWords = - SpeechRecognitionWords(secondRecognizedWords, secondConfidence); + SpeechRecognitionWords(secondRecognizedWords, null, secondConfidence); static final SpeechRecognitionResult firstRecognizedResult = SpeechRecognitionResult([firstWords], false); static final SpeechRecognitionResult secondRecognizedResult =