From 633545a460677ffdc8f847385e566dcfa998fa6a Mon Sep 17 00:00:00 2001 From: herring101 Date: Sat, 11 Jun 2022 11:13:09 +0900 Subject: [PATCH 1/3] #7 delete english comment --- lib/main.dart | 70 --------------------------------------------------- 1 file changed, 70 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index 2947234..ffb5144 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -10,21 +10,11 @@ void main() async { class MyApp extends StatelessWidget { const MyApp({Key? key}) : super(key: key); - // This widget is the root of your application. @override Widget build(BuildContext context) { return MaterialApp( title: 'Flutter Demo', theme: ThemeData( - // This is the theme of your application. - // - // Try running your application with "flutter run". You'll see the - // application has a blue toolbar. Then, without quitting the app, try - // changing the primarySwatch below to Colors.green and then invoke - // "hot reload" (press "r" in the console where you ran "flutter run", - // or simply save your changes to "hot reload" in a Flutter IDE). - // Notice that the counter didn't reset back to zero; the application - // is not restarted. primarySwatch: Colors.blue, ), home: const MyHomePage(title: 'Flutter Demo Home Page'), @@ -34,16 +24,6 @@ class MyApp extends StatelessWidget { class MyHomePage extends StatefulWidget { const MyHomePage({Key? key, required this.title}) : super(key: key); - - // This widget is the home page of your application. It is stateful, meaning - // that it has a State object (defined below) that contains fields that affect - // how it looks. - - // This class is the configuration for the state. It holds the values (in this - // case the title) provided by the parent (in this case the App widget) and - // used by the build method of the State. Fields in a Widget subclass are - // always marked "final". - final String title; @override @@ -51,60 +31,16 @@ class MyHomePage extends StatefulWidget { } class _MyHomePageState extends State { - int _counter = 0; - - void _incrementCounter() { - setState(() { - // This call to setState tells the Flutter framework that something has - // changed in this State, which causes it to rerun the build method below - // so that the display can reflect the updated values. If we changed - // _counter without calling setState(), then the build method would not be - // called again, and so nothing would appear to happen. - _counter++; - }); - } - @override Widget build(BuildContext context) { - // This method is rerun every time setState is called, for instance as done - // by the _incrementCounter method above. - // - // The Flutter framework has been optimized to make rerunning build methods - // fast, so that you can just rebuild anything that needs updating rather - // than having to individually change instances of widgets. return Scaffold( appBar: AppBar( - // Here we take the value from the MyHomePage object that was created by - // the App.build method, and use it to set our appbar title. title: Text(widget.title), ), body: Center( - // Center is a layout widget. It takes a single child and positions it - // in the middle of the parent. child: Column( - // Column is also a layout widget. It takes a list of children and - // arranges them vertically. By default, it sizes itself to fit its - // children horizontally, and tries to be as tall as its parent. - // - // Invoke "debug painting" (press "p" in the console, choose the - // "Toggle Debug Paint" action from the Flutter Inspector in Android - // Studio, or the "Toggle Debug Paint" command in Visual Studio Code) - // to see the wireframe for each widget. - // - // Column has various properties to control how it sizes itself and - // how it positions its children. Here we use mainAxisAlignment to - // center the children vertically; the main axis here is the vertical - // axis because Columns are vertical (the cross axis would be - // horizontal). mainAxisAlignment: MainAxisAlignment.center, children: [ - const Text( - 'You have pushed the button this many times:', - ), - Text( - '$_counter', - style: Theme.of(context).textTheme.headline4, - ), FloatingActionButton( //RingingAlarmPageに飛ぶボタン heroTag: 'ringing_alarm_page_button', @@ -122,12 +58,6 @@ class _MyHomePageState extends State { ], ), ), - floatingActionButton: FloatingActionButton( - heroTag: 'increment_button', - onPressed: _incrementCounter, - tooltip: 'Increment', - child: const Icon(Icons.add), - ), // This trailing comma makes auto-formatting nicer for build methods. ); } } From 6147d5e39bb250f389cb436f7be4e78a8185b059 Mon Sep 17 00:00:00 2001 From: MrMocchy Date: Sun, 12 Jun 2022 11:21:27 +0900 Subject: [PATCH 2/3] =?UTF-8?q?#10-=E6=97=A9=E5=8F=A3=E8=A8=80=E8=91=89?= =?UTF-8?q?=E3=81=AE=E9=9F=B3=E5=A3=B0=E8=AA=8D=E8=AD=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 音声認識パッケージspeech_to_textの導入 マイクの権限要求 mainにテスト用のボタンとテキスト追加 音声認識のクラス2つ(viewとmodel)と問題用のクラスを作成 --- android/app/build.gradle | 2 +- android/app/src/main/AndroidManifest.xml | 12 +++ lib/main.dart | 19 +++++ lib/model/tongue_twister_model.dart | 78 ++++++++++++++++++++ lib/model/tongue_twister_questions.dart | 41 +++++++++++ lib/view/tongue_twister_view.dart | 94 ++++++++++++++++++++++++ pubspec.lock | 40 ++++++++++ pubspec.yaml | 1 + 8 files changed, 286 insertions(+), 1 deletion(-) create mode 100644 lib/model/tongue_twister_model.dart create mode 100644 lib/model/tongue_twister_questions.dart create mode 100644 lib/view/tongue_twister_view.dart diff --git a/android/app/build.gradle b/android/app/build.gradle index fe5af34..3619428 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -47,7 +47,7 @@ android { applicationId "com.example.alarm2022" // You can update the following values to match your application needs. // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-build-configuration. - minSdkVersion flutter.minSdkVersion + minSdkVersion 21//flutter.minSdkVersion targetSdkVersion flutter.targetSdkVersion versionCode flutterVersionCode.toInteger() versionName flutterVersionName diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 58143ab..a589559 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -48,4 +48,16 @@ + + + + + + + + + + + + diff --git a/lib/main.dart b/lib/main.dart index ffb5144..3467419 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'model/ringing_alarm_model.dart'; import 'view/ringing_alarm_view.dart'; +import 'view/tongue_twister_view.dart'; void main() async { setupRingingAlarm(); @@ -31,6 +32,8 @@ class MyHomePage extends StatefulWidget { } class _MyHomePageState extends State { + bool? ttIsCleared; + @override Widget build(BuildContext context) { return Scaffold( @@ -55,6 +58,22 @@ class _MyHomePageState extends State { }, child: const Icon(Icons.alarm), ), + FloatingActionButton( + heroTag: 'tongue_twister_page_button', + onPressed: () async { + ttIsCleared = await Navigator.of(context).push( + MaterialPageRoute(builder: (context) { + return const TongueTwisterPage(); + }), + ); + setState(() {}); + }, + child: const Icon(Icons.mic), + ), + Text( + "早口言葉:${ttIsCleared == null ? "未成功" : (ttIsCleared! ? "成功" : "失敗")}", + style: Theme.of(context).textTheme.headline5, + ) ], ), ), diff --git a/lib/model/tongue_twister_model.dart b/lib/model/tongue_twister_model.dart new file mode 100644 index 0000000..a6b6a68 --- /dev/null +++ b/lib/model/tongue_twister_model.dart @@ -0,0 +1,78 @@ +import 'package:flutter/material.dart'; +import 'dart:math'; +import 'package:speech_to_text/speech_to_text.dart' as stt; +import 'package:speech_to_text/speech_recognition_error.dart'; +import 'package:speech_to_text/speech_recognition_result.dart'; +import 'package:alarm2022/model/tongue_twister_questions.dart'; + +class TongueTwisterModel { + String lastWords = ""; + String lastError = ''; + String lastStatus = ''; + stt.SpeechToText speech = stt.SpeechToText(); + + late DateTime? _startTime; + late DateTime? _endTime; + Duration speakDuration = const Duration(); + String message = ""; + bool isDoneLastTime = false; + + late TtQuestion question; + + Future speak() async { + bool available = await speech.initialize( + onError: errorListener, onStatus: statusListener); + if (available) { + _startTime = null; + speech.listen(onResult: resultListener, localeId: question.language); + } else { + debugPrint("The user has denied the use of speech recognition."); + } + } + + Future stop() async { + speech.stop(); + } + + void resultListener(SpeechRecognitionResult result) { + _startTime ??= DateTime.now(); + _endTime = DateTime.now(); + speakDuration = _endTime!.difference(_startTime!); + lastWords = result.recognizedWords; + } + + void errorListener(SpeechRecognitionError error) { + lastError = '${error.errorMsg} - ${error.permanent}'; + } + + void statusListener(String status) { + lastStatus = status; + } + + void resetQuestion() { + lastWords = ""; + speakDuration = const Duration(); + message = ""; + question = ttQuestions[Random().nextInt(ttQuestions.length)]; + } + + void doneCheck() { + bool last = isDoneLastTime; + isDoneLastTime = speech.lastStatus == "done"; + if (last == false && isDoneLastTime) { + if (lastWords.isEmpty) return; + if (lastWords == question.pronounceQuestion) { + message = "一致"; + return; + } + if (speakDuration.compareTo(question.limitTime) > 0) { + message = "遅い"; + return; + } + if (lastWords != question.pronounceQuestion) { + message = "違う"; + return; + } + } + } +} diff --git a/lib/model/tongue_twister_questions.dart b/lib/model/tongue_twister_questions.dart new file mode 100644 index 0000000..e5c622e --- /dev/null +++ b/lib/model/tongue_twister_questions.dart @@ -0,0 +1,41 @@ +import 'package:flutter/material.dart'; + +class TtQuestion { + TtQuestion(this.displayQuestion, this.limitSecond, + {this.pronounce, this.english = false}); + final String displayQuestion; + final double limitSecond; + + ///初期化専用。pronounceQuestionを使うこと。 + final String? pronounce; + final bool english; + + Duration get limitTime { + return Duration(seconds: limitSecond.toInt()) + + Duration( + milliseconds: ((limitSecond - limitSecond.toInt()) * 1000).toInt()); + } + + String get pronounceQuestion { + return pronounce ?? displayQuestion; + } + + String? get language { + return english ? const Locale('en').toLanguageTag() : null; + } +} + +List ttQuestions = [ + TtQuestion("生麦生米生卵", 2.5), + TtQuestion("隣の客はよく柿食う客だ", 2.5), + TtQuestion("スモモも桃も桃のうち", 2.5, pronounce: "すもももももももものうち"), + TtQuestion("東京特許許可局長今日急遽休暇許可拒否", 4), + TtQuestion("I scream, you scream, we all scream for ice cream!", 3.5, + pronounce: "I scream you scream we all scream for ice cream", + english: true), + TtQuestion( + "パブロ・ディエゴ・ホセ・フランシスコ・デ・パウラ・ホアン・ネポムセーノ・チプリアーノ・デ・ラ・サンティシマ・トリニダード・ルイス・ピカソ", + 9, + pronounce: "パブロディエゴホセフランシスコデパウラホアンネポムセーノチプリアーニデラサンティシマトリニダードルイスピカソ", + ), +]; diff --git a/lib/view/tongue_twister_view.dart b/lib/view/tongue_twister_view.dart new file mode 100644 index 0000000..280be19 --- /dev/null +++ b/lib/view/tongue_twister_view.dart @@ -0,0 +1,94 @@ +import 'package:flutter/material.dart'; +import 'dart:async'; +import 'package:alarm2022/model/tongue_twister_model.dart'; + +class TongueTwisterPage extends StatefulWidget { + const TongueTwisterPage({Key? key, this.title}) : super(key: key); + final String? title; + @override + // ignore: library_private_types_in_public_api + _TongueTwisterPageState createState() => _TongueTwisterPageState(); +} + +class _TongueTwisterPageState extends State { + TongueTwisterModel ttm = TongueTwisterModel(); + + @override + void initState() { + super.initState(); + ttm.resetQuestion(); + Timer.periodic(const Duration(milliseconds: 100), (Timer clockTimer) { + if (!mounted) { + return; + } + setState(() { + ttm.doneCheck(); + }); + }); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text("Tongue Twister"), + ), + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + 'お題', + style: Theme.of(context).textTheme.headline5, + ), + Text( + ttm.question.displayQuestion, + style: Theme.of(context).textTheme.headline5, + ), + Text( + '認識', + style: Theme.of(context).textTheme.headline5, + ), + Text( + ttm.lastWords, + style: Theme.of(context).textTheme.headline5, + ), + Text( + 'ステータス : ${ttm.speech.lastStatus}', + style: Theme.of(context).textTheme.headline5, + ), + Text( + '時間 : ${ttm.speakDuration.inMilliseconds ~/ 100 / 10} s', + style: Theme.of(context).textTheme.headline6, + ), + Text( + '評価:${ttm.message}', + style: Theme.of(context).textTheme.headline6, + ), + ], + ), + ), + floatingActionButton: + Row(mainAxisAlignment: MainAxisAlignment.end, children: [ + FloatingActionButton( + heroTag: "tt_back", + onPressed: () { + Navigator.of(context).pop(ttm.message == "一致"); + }, + child: const Icon(Icons.arrow_back)), + FloatingActionButton( + heroTag: "tt_changeQuestion", + onPressed: ttm.resetQuestion, + child: const Icon(Icons.cached)), + FloatingActionButton( + heroTag: "tt_play", + onPressed: ttm.speak, + child: const Icon(Icons.play_arrow)), + FloatingActionButton( + heroTag: "tt_stop", + onPressed: ttm.stop, + child: const Icon(Icons.stop)) + ]), + ); + } +} diff --git a/pubspec.lock b/pubspec.lock index 4f887e1..6c8132a 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -123,6 +123,11 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" intl: dependency: "direct main" description: @@ -130,6 +135,20 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.17.0" + js: + dependency: transitive + description: + name: js + url: "https://pub.dartlang.org" + source: hosted + version: "0.6.4" + json_annotation: + dependency: transitive + description: + name: json_annotation + url: "https://pub.dartlang.org" + source: hosted + version: "4.5.0" lints: dependency: transitive description: @@ -172,6 +191,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.8.1" + pedantic: + dependency: transitive + description: + name: pedantic + url: "https://pub.dartlang.org" + source: hosted + version: "1.11.1" petitparser: dependency: transitive description: @@ -212,6 +238,20 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.8.2" + speech_to_text: + dependency: "direct main" + description: + name: speech_to_text + url: "https://pub.dartlang.org" + source: hosted + version: "5.5.0" + speech_to_text_platform_interface: + dependency: transitive + description: + name: speech_to_text_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.1" stack_trace: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 4a2e95d..7aa66e6 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -38,6 +38,7 @@ dependencies: intl: ^0.17.0 flutter_local_notifications: ^9.6.0 timezone: ^0.8.0 + speech_to_text: ^5.5.0 dev_dependencies: flutter_test: From cf278b9d7aa09796a5629e6d5c8f420248908bf6 Mon Sep 17 00:00:00 2001 From: MrMocchy Date: Sun, 12 Jun 2022 12:46:49 +0900 Subject: [PATCH 3/3] =?UTF-8?q?#10-=E6=97=A9=E5=8F=A3=E8=A8=80=E8=91=89?= =?UTF-8?q?=E3=80=80=E3=82=B3=E3=83=A1=E3=83=B3=E3=83=88=E3=82=92=E8=BF=BD?= =?UTF-8?q?=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/main.dart | 2 +- lib/model/tongue_twister_model.dart | 37 ++++++++++++++++++++----- lib/model/tongue_twister_questions.dart | 23 +++++++++++++-- lib/view/tongue_twister_view.dart | 12 ++++++-- 4 files changed, 60 insertions(+), 14 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index 3467419..78b474b 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -72,7 +72,7 @@ class _MyHomePageState extends State { ), Text( "早口言葉:${ttIsCleared == null ? "未成功" : (ttIsCleared! ? "成功" : "失敗")}", - style: Theme.of(context).textTheme.headline5, + style: Theme.of(context).textTheme.headline6, ) ], ), diff --git a/lib/model/tongue_twister_model.dart b/lib/model/tongue_twister_model.dart index a6b6a68..9e971f2 100644 --- a/lib/model/tongue_twister_model.dart +++ b/lib/model/tongue_twister_model.dart @@ -6,23 +6,33 @@ import 'package:speech_to_text/speech_recognition_result.dart'; import 'package:alarm2022/model/tongue_twister_questions.dart'; class TongueTwisterModel { - String lastWords = ""; - String lastError = ''; - String lastStatus = ''; + //音声認識の実体 stt.SpeechToText speech = stt.SpeechToText(); +//表示用String + String lastWords = ""; + String lastError = ""; + String lastStatus = ""; + + //時間計測用 late DateTime? _startTime; late DateTime? _endTime; Duration speakDuration = const Duration(); + //判定表示用 String message = ""; bool isDoneLastTime = false; +//お題。RtQuestionを参照 late TtQuestion question; +//音声認識開始 Future speak() async { + //権限があるかなどを確認し初期化 + //アプリ起動につき1回のみ行う bool available = await speech.initialize( onError: errorListener, onStatus: statusListener); if (available) { + //音声認識できるなら計測時間初期化して開始 _startTime = null; speech.listen(onResult: resultListener, localeId: question.language); } else { @@ -30,25 +40,33 @@ class TongueTwisterModel { } } +//音声認識終了。Androidでは一定時間話さなかったら自動的に終了するのであまり使われない Future stop() async { speech.stop(); } +//音声認識の結果を受け取る void resultListener(SpeechRecognitionResult result) { _startTime ??= DateTime.now(); _endTime = DateTime.now(); + //経過時間を測定 speakDuration = _endTime!.difference(_startTime!); + //認識結果を保存 lastWords = result.recognizedWords; } +//エラー時 void errorListener(SpeechRecognitionError error) { lastError = '${error.errorMsg} - ${error.permanent}'; } +//状態(String)を受け取る。 +//notListening, listening, done の3種 void statusListener(String status) { lastStatus = status; } +//お題を変更。合わせて変数を初期化 void resetQuestion() { lastWords = ""; speakDuration = const Duration(); @@ -56,23 +74,28 @@ class TongueTwisterModel { question = ttQuestions[Random().nextInt(ttQuestions.length)]; } +//音声認識が終了したか判断し評価を下す。終了判断はlastStatusの監視という力技 void doneCheck() { + //前回の状態を更新 bool last = isDoneLastTime; isDoneLastTime = speech.lastStatus == "done"; + //今回doneになったら終了と判断 if (last == false && isDoneLastTime) { if (lastWords.isEmpty) return; + //判断部分 + //文字表示だけでなく成功時のエフェクトなども作りたい if (lastWords == question.pronounceQuestion) { message = "一致"; return; } - if (speakDuration.compareTo(question.limitTime) > 0) { - message = "遅い"; - return; - } if (lastWords != question.pronounceQuestion) { message = "違う"; return; } + if (speakDuration.compareTo(question.limitTime) > 0) { + message = "遅い"; + return; + } } } } diff --git a/lib/model/tongue_twister_questions.dart b/lib/model/tongue_twister_questions.dart index e5c622e..9baae6d 100644 --- a/lib/model/tongue_twister_questions.dart +++ b/lib/model/tongue_twister_questions.dart @@ -1,30 +1,47 @@ import 'package:flutter/material.dart'; class TtQuestion { - TtQuestion(this.displayQuestion, this.limitSecond, - {this.pronounce, this.english = false}); + ///お題 + TtQuestion( + this.displayQuestion, + this.limitSecond, { + this.pronounce, + this.english = false, + }); + + ///お題の文 final String displayQuestion; + + ///制限時間 + ///- 初期化専用。limitTimeを使うこと。 final double limitSecond; - ///初期化専用。pronounceQuestionを使うこと。 + ///音声認識結果がお題の分と違ってしまうとき(変換、句読点など)の認識される文 + ///- 初期化専用。pronounceQuestionを使うこと。 final String? pronounce; + + ///英語で認識 final bool english; + ///制限時間(参照用) Duration get limitTime { return Duration(seconds: limitSecond.toInt()) + Duration( milliseconds: ((limitSecond - limitSecond.toInt()) * 1000).toInt()); } + ///音声認識結果がお題の分と違ってしまうとき(変換、句読点など)の認識される文 String get pronounceQuestion { return pronounce ?? displayQuestion; } + ///認識する言語 String? get language { return english ? const Locale('en').toLanguageTag() : null; } } +///問題文一覧 List ttQuestions = [ TtQuestion("生麦生米生卵", 2.5), TtQuestion("隣の客はよく柿食う客だ", 2.5), diff --git a/lib/view/tongue_twister_view.dart b/lib/view/tongue_twister_view.dart index 280be19..e1d9bbb 100644 --- a/lib/view/tongue_twister_view.dart +++ b/lib/view/tongue_twister_view.dart @@ -11,16 +11,22 @@ class TongueTwisterPage extends StatefulWidget { } class _TongueTwisterPageState extends State { + //modelの方の実体 TongueTwisterModel ttm = TongueTwisterModel(); + @override + void setState(fn) { + if (mounted) { + super.setState(fn); + } + } + @override void initState() { super.initState(); ttm.resetQuestion(); + //表示更新用タイマー Timer.periodic(const Duration(milliseconds: 100), (Timer clockTimer) { - if (!mounted) { - return; - } setState(() { ttm.doneCheck(); });