├── .gitignore ├── .metadata ├── LICENSE ├── README.md ├── Screenshot_homepage.jpg ├── android ├── .gitignore ├── app │ ├── build.gradle │ ├── proguard-rules.pro │ └── src │ │ ├── debug │ │ └── AndroidManifest.xml │ │ ├── main │ │ ├── AndroidManifest.xml │ │ ├── java │ │ │ └── com │ │ │ │ └── example │ │ │ │ └── rhasspy_mobile_app │ │ │ │ ├── MainActivity.java │ │ │ │ ├── StartRecordingAppWidgetProvider.java │ │ │ │ └── WakeWordService.java │ │ └── res │ │ │ ├── drawable │ │ │ ├── app_icon.png │ │ │ └── launch_background.xml │ │ │ ├── layout │ │ │ └── start_recording_widget.xml │ │ │ ├── mipmap-hdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-mdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xhdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xxhdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xxxhdpi │ │ │ └── ic_launcher.png │ │ │ ├── values │ │ │ └── styles.xml │ │ │ └── xml │ │ │ └── start_recoding_appwidget_info.xml │ │ └── profile │ │ └── AndroidManifest.xml ├── build.gradle ├── gradle.properties ├── gradle │ └── wrapper │ │ └── gradle-wrapper.properties ├── settings.gradle └── settings_aar.gradle ├── assets └── images │ └── rhasspy.png ├── ios ├── .gitignore ├── Flutter │ ├── AppFrameworkInfo.plist │ ├── Debug.xcconfig │ └── Release.xcconfig ├── Runner.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ ├── IDEWorkspaceChecks.plist │ │ │ └── WorkspaceSettings.xcsettings │ └── xcshareddata │ │ └── xcschemes │ │ └── Runner.xcscheme ├── Runner.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── WorkspaceSettings.xcsettings └── Runner │ ├── AppDelegate.swift │ ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ ├── Contents.json │ │ ├── Icon-App-1024x1024@1x.png │ │ ├── Icon-App-20x20@1x.png │ │ ├── Icon-App-20x20@2x.png │ │ ├── Icon-App-20x20@3x.png │ │ ├── Icon-App-29x29@1x.png │ │ ├── Icon-App-29x29@2x.png │ │ ├── Icon-App-29x29@3x.png │ │ ├── Icon-App-40x40@1x.png │ │ ├── Icon-App-40x40@2x.png │ │ ├── Icon-App-40x40@3x.png │ │ ├── Icon-App-50x50@1x.png │ │ ├── Icon-App-50x50@2x.png │ │ ├── Icon-App-57x57@1x.png │ │ ├── Icon-App-57x57@2x.png │ │ ├── Icon-App-60x60@2x.png │ │ ├── Icon-App-60x60@3x.png │ │ ├── Icon-App-72x72@1x.png │ │ ├── Icon-App-72x72@2x.png │ │ ├── Icon-App-76x76@1x.png │ │ ├── Icon-App-76x76@2x.png │ │ └── Icon-App-83.5x83.5@2x.png │ └── LaunchImage.imageset │ │ ├── Contents.json │ │ ├── LaunchImage.png │ │ ├── LaunchImage@2x.png │ │ ├── LaunchImage@3x.png │ │ └── README.md │ ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard │ ├── Info.plist │ └── Runner-Bridging-Header.h ├── lib ├── main.dart ├── rhasspy_dart │ ├── exceptions.dart │ ├── parse_messages.dart │ ├── rhasspy_api.dart │ ├── rhasspy_mqtt_api.dart │ ├── rhasspy_mqtt_isolate.dart │ └── utility │ │ └── rhasspy_mqtt_logger.dart ├── screens │ ├── app_settings.dart │ └── home_page.dart ├── utils │ ├── audio_recorder_isolate.dart │ ├── constants.dart │ ├── logger │ │ ├── log_page.dart │ │ └── logger.dart │ └── utils.dart ├── wake_word │ ├── udp_wake_word.dart │ ├── wake_word_base.dart │ └── wake_word_utils.dart └── widget │ └── Intent_viewer.dart ├── pubspec.lock ├── pubspec.yaml ├── test ├── rhasspy_api_test.dart ├── rhasspy_mqtt_test.dart ├── utils_test.dart └── widget_test.dart └── test_resources └── audio ├── inputAudio1 └── outputAudio1.wav /.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | .vscode 12 | # IntelliJ related 13 | *.iml 14 | *.ipr 15 | *.iws 16 | .idea/ 17 | 18 | # The .vscode folder contains launch configuration and tasks you configure in 19 | # VS Code which you may wish to be included in version control, so this line 20 | # is commented out by default. 21 | #.vscode/ 22 | 23 | # Flutter/Dart/Pub related 24 | **/doc/api/ 25 | .dart_tool/ 26 | .flutter-plugins 27 | .flutter-plugins-dependencies 28 | .packages 29 | .pub-cache/ 30 | .pub/ 31 | /build/ 32 | 33 | # Web related 34 | lib/generated_plugin_registrant.dart 35 | 36 | # Symbolication related 37 | app.*.symbols 38 | 39 | # Obfuscation related 40 | app.*.map.json 41 | 42 | # Exceptions to above rules. 43 | !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages 44 | -------------------------------------------------------------------------------- /.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: b041144f833e05cf463b8887fa12efdec9493488 8 | channel: stable 9 | 10 | project_type: app 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 razzo04 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Rhasspy mobile app 2 | 3 | This is a simple mobile app that interfaces with rhasspy. 4 | 5 | 8 | 11 | # Features 12 | - Text to speak 13 | - Speech to text 14 | - ability to transcribe audio 15 | - Ssl connection and possibility to set self-signed certificates 16 | - Support Hermes protocol 17 | - Wake word over UDP 18 | - Android widget for listen to a command 19 | 20 | # Getting Started 21 | For android you can install the app by downloading the file with extension .apk present in each new [release](https://github.com/razzo04/rhasspy-mobile-app/releases) and then open it in your phone after accepting the installation from unknown sources. It is not yet available for ios. 22 | 23 | Once the app has been installed, it needs to be configured from version 1.7.0, the configuration of the app has been greatly simplified it is sufficient to insert in the text field called "Rhasspy ip" the ip and the port where rhasspy is running. If you are using the default port it will only be necessary to enter the ip. Once the entry is confirmed, a message should appear indicating whether a connection to rhasspy has occurred. If not, check the SSL settings and the logs which may contain useful information to understand the nature of the problem. Once you have made a connection to rhasspy you can click the auto setup button this will take care of generating a siteId if not specified and taking the MQTT credentials and adding the siteId to the various services so that the app can work. If the procedure does not work, check the logs and open an issue if necessary. If rhasspy does not have MQTT credentials, the app will check if it has them and if so it will send them and complete the setup procedure. 24 | 25 | # Building From Source 26 | To get started you need to install [flutter](https://flutter.dev/docs/get-started/install) and then you can download the repository. 27 | ```bash 28 | git clone https://github.com/razzo04/rhasspy-mobile-app.git 29 | cd rhasspy-mobile-app 30 | ``` 31 | For build android. 32 | ```bash 33 | flutter build apk 34 | ``` 35 | For build ios you need macOS and Xcode. 36 | ```bash 37 | flutter build ios 38 | ``` 39 | -------------------------------------------------------------------------------- /Screenshot_homepage.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/razzo04/rhasspy-mobile-app/3c59971270eab0278cd5dbf6adac4064b5f14908/Screenshot_homepage.jpg -------------------------------------------------------------------------------- /android/.gitignore: -------------------------------------------------------------------------------- 1 | gradle-wrapper.jar 2 | /.gradle 3 | /captures/ 4 | /gradlew 5 | /gradlew.bat 6 | /local.properties 7 | GeneratedPluginRegistrant.java 8 | -------------------------------------------------------------------------------- /android/app/build.gradle: -------------------------------------------------------------------------------- 1 | def localProperties = new Properties() 2 | def localPropertiesFile = rootProject.file('local.properties') 3 | if (localPropertiesFile.exists()) { 4 | localPropertiesFile.withReader('UTF-8') { reader -> 5 | localProperties.load(reader) 6 | } 7 | } 8 | 9 | def flutterRoot = localProperties.getProperty('flutter.sdk') 10 | if (flutterRoot == null) { 11 | throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") 12 | } 13 | 14 | def flutterVersionCode = localProperties.getProperty('flutter.versionCode') 15 | if (flutterVersionCode == null) { 16 | flutterVersionCode = '1' 17 | } 18 | 19 | def flutterVersionName = localProperties.getProperty('flutter.versionName') 20 | if (flutterVersionName == null) { 21 | flutterVersionName = '1.0' 22 | } 23 | 24 | apply plugin: 'com.android.application' 25 | apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" 26 | 27 | android { 28 | compileSdkVersion 30 29 | 30 | lintOptions { 31 | disable 'InvalidPackage' 32 | } 33 | 34 | defaultConfig { 35 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). 36 | applicationId "com.example.rhasspy_mobile_app" 37 | minSdkVersion 16 38 | targetSdkVersion 30 39 | versionCode flutterVersionCode.toInteger() 40 | versionName flutterVersionName 41 | } 42 | 43 | buildTypes { 44 | release { 45 | // TODO: Add your own signing config for the release build. 46 | // Signing with the debug keys for now, so `flutter run --release` works. 47 | signingConfig signingConfigs.debug 48 | } 49 | } 50 | } 51 | 52 | flutter { 53 | source '../..' 54 | } 55 | -------------------------------------------------------------------------------- /android/app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | 2 | ## Flutter wrapper 3 | -keep class io.flutter.app.** { *; } 4 | -keep class io.flutter.plugin.** { *; } 5 | -keep class io.flutter.util.** { *; } 6 | -keep class io.flutter.view.** { *; } 7 | -keep class io.flutter.** { *; } 8 | -keep class io.flutter.plugins.** { *; } 9 | -dontwarn io.flutter.embedding.** 10 | 11 | ## Gson rules 12 | # Gson uses generic type information stored in a class file when working with fields. Proguard 13 | # removes such information by default, so configure it to keep all of it. 14 | -keepattributes Signature 15 | 16 | # For using GSON @Expose annotation 17 | -keepattributes *Annotation* 18 | 19 | # Gson specific classes 20 | -dontwarn sun.misc.** 21 | #-keep class com.google.gson.stream.** { *; } 22 | 23 | # Prevent proguard from stripping interface information from TypeAdapter, TypeAdapterFactory, 24 | # JsonSerializer, JsonDeserializer instances (so they can be used in @JsonAdapter) 25 | -keep class * implements com.google.gson.TypeAdapter 26 | -keep class * implements com.google.gson.TypeAdapterFactory 27 | -keep class * implements com.google.gson.JsonSerializer 28 | -keep class * implements com.google.gson.JsonDeserializer 29 | 30 | # Prevent R8 from leaving Data object members always null 31 | -keepclassmembers,allowobfuscation class * { 32 | @com.google.gson.annotations.SerializedName ; 33 | } 34 | 35 | ## flutter_local_notification plugin rules 36 | -keep class com.dexterous.** { *; } -------------------------------------------------------------------------------- /android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 20 | 23 | 30 | 34 | 38 | 43 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 60 | 61 | 63 | 66 | 67 | -------------------------------------------------------------------------------- /android/app/src/main/java/com/example/rhasspy_mobile_app/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.example.rhasspy_mobile_app; 2 | 3 | import android.content.ComponentName; 4 | import android.content.Context; 5 | import android.content.Intent; 6 | import android.content.ServiceConnection; 7 | import android.os.Bundle; 8 | import android.os.Handler; 9 | import android.os.IBinder; 10 | import android.os.PersistableBundle; 11 | import android.util.Log; 12 | 13 | import androidx.annotation.NonNull; 14 | import androidx.annotation.Nullable; 15 | import androidx.core.content.ContextCompat; 16 | 17 | import java.util.Arrays; 18 | import java.util.List; 19 | import java.util.Objects; 20 | 21 | import io.flutter.FlutterInjector; 22 | import io.flutter.embedding.android.FlutterActivity; 23 | import io.flutter.embedding.engine.FlutterEngine; 24 | import io.flutter.embedding.engine.dart.DartExecutor; 25 | import io.flutter.embedding.engine.loader.FlutterLoader; 26 | import io.flutter.plugin.common.MethodChannel; 27 | import io.flutter.view.FlutterMain; 28 | 29 | public class MainActivity extends FlutterActivity { 30 | WakeWordService mService; 31 | private static final String CHANNEL = "rhasspy_mobile_app/widget"; 32 | boolean mBound; 33 | private MethodChannel channel; 34 | public MethodChannel channel2; 35 | public MethodChannel wakeWordChannel; 36 | 37 | @Override 38 | public void onCreate(@Nullable Bundle savedInstanceState, @Nullable PersistableBundle persistentState) { 39 | super.onCreate(savedInstanceState, persistentState); 40 | if (!isTaskRoot() 41 | && getIntent().hasCategory(Intent.CATEGORY_LAUNCHER) 42 | && getIntent().getAction() != null 43 | && getIntent().getAction().equals(Intent.ACTION_MAIN)) { 44 | finish(); 45 | } 46 | 47 | 48 | } 49 | 50 | @Override 51 | public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) { 52 | super.configureFlutterEngine(flutterEngine); 53 | channel = new MethodChannel(flutterEngine.getDartExecutor(), CHANNEL); 54 | Bundle extras = getIntent().getExtras(); 55 | if(extras != null && extras.containsKey("StartRecording")) { 56 | Log.i("Home","Starting recording"); 57 | DartExecutor.DartEntrypoint entryPoint = new DartExecutor.DartEntrypoint(FlutterInjector.instance().flutterLoader().findAppBundlePath(),"main"); 58 | flutterEngine.getDartExecutor().executeDartEntrypoint(entryPoint); 59 | channel.invokeMethod("StartRecording", null); 60 | } 61 | 62 | channel2 =new MethodChannel(flutterEngine.getDartExecutor().getBinaryMessenger(), "rhasspy_mobile_app"); 63 | channel2.setMethodCallHandler((call, result) -> { 64 | if(call.method.equals("sendToBackground")){ 65 | Log.i("Background", "sendToBackground"); 66 | moveTaskToBack(true); 67 | } 68 | }); 69 | wakeWordChannel =new MethodChannel(flutterEngine.getDartExecutor().getBinaryMessenger(), "wake_word"); 70 | wakeWordChannel.setMethodCallHandler((call, result) -> { 71 | switch (call.method){ 72 | case "stop": 73 | Log.i("WaKeWord", "Stopping the foreground-thread"); 74 | //mService.stopService(); 75 | unbindService(connection); 76 | mBound = false; 77 | Intent intent = new Intent(getApplicationContext(), WakeWordService.class); 78 | intent.setAction("Stop"); 79 | ContextCompat.startForegroundService(getApplicationContext(), intent); 80 | result.success(true); 81 | break; 82 | case "pause": 83 | if(mBound){ 84 | mService.pause(); 85 | result.success(true); 86 | } else { 87 | result.error("NoRunningService","no service running",null); 88 | } 89 | break; 90 | case "resume": 91 | if(mBound){ 92 | mService.resume(); 93 | result.success(true); 94 | } else { 95 | result.error("NoRunningService","no service running",null); 96 | } 97 | break; 98 | case "start": 99 | Log.i("WaKeWord", "Starting the foreground-thread"); 100 | Intent serviceIntent = new Intent(getActivity().getApplicationContext(), WakeWordService.class); 101 | serviceIntent.putExtra("wakeWordDetector",call.argument("wakeWordDetector").toString()); 102 | switch (call.argument("wakeWordDetector").toString()){ 103 | case "UDP": 104 | serviceIntent.putExtra("ip", call.argument("ip").toString()); 105 | serviceIntent.putExtra("port", Integer.parseInt(call.argument("port").toString())); 106 | break; 107 | default: 108 | result.error("UnsupportedWakeWord",null,null); 109 | return; 110 | } 111 | 112 | ContextCompat.startForegroundService(getActivity(), serviceIntent); 113 | bindService(serviceIntent, connection,BIND_IMPORTANT); 114 | result.success(true); 115 | break; 116 | case "isRunning": 117 | Log.i("WaKeWord", "check if is listening"); 118 | 119 | if(!mBound || mService == null){ 120 | result.success(false); 121 | } else { 122 | result.success(true); 123 | } 124 | break; 125 | case "isListening": 126 | Log.i("WaKeWord", "check if is listening"); 127 | 128 | if(!mBound || mService == null){ 129 | result.success(false); 130 | } else { 131 | result.success(!mService.isPaused); 132 | } 133 | break; 134 | case "getWakeWordDetector": 135 | List availableWakeWordDetector = Arrays.asList("UDP"); 136 | result.success(availableWakeWordDetector); 137 | break; 138 | } 139 | }); 140 | } 141 | 142 | 143 | @Override 144 | protected void onNewIntent(@NonNull Intent intent) { 145 | super.onNewIntent(intent); 146 | Log.i("Home", "NewIntent"); 147 | if(intent.getExtras() != null) { 148 | if (Objects.requireNonNull(intent.getExtras()).containsKey("StartRecording")) { 149 | Log.i("Home", "StartRecording"); 150 | channel.invokeMethod("StartRecording", null); 151 | } 152 | } 153 | 154 | 155 | } 156 | private ServiceConnection connection = new ServiceConnection() { 157 | 158 | @Override 159 | public void onServiceConnected(ComponentName className, 160 | IBinder service) { 161 | WakeWordService.LocalBinder binder = (WakeWordService.LocalBinder) service; 162 | mService = binder.getService(); 163 | mBound = true; 164 | } 165 | 166 | @Override 167 | public void onServiceDisconnected(ComponentName className) { 168 | mBound = false; 169 | } 170 | }; 171 | 172 | @Override 173 | protected void onDestroy() { 174 | if(mBound || mService != null) unbindService(connection); 175 | super.onDestroy(); 176 | 177 | } 178 | } 179 | 180 | -------------------------------------------------------------------------------- /android/app/src/main/java/com/example/rhasspy_mobile_app/StartRecordingAppWidgetProvider.java: -------------------------------------------------------------------------------- 1 | package com.example.rhasspy_mobile_app; 2 | 3 | import android.app.PendingIntent; 4 | import android.appwidget.AppWidgetManager; 5 | import android.appwidget.AppWidgetProvider; 6 | import android.content.Context; 7 | import android.content.Intent; 8 | import android.widget.RemoteViews; 9 | 10 | import android.util.Log; 11 | import io.flutter.plugin.common.MethodChannel; 12 | import io.flutter.view.FlutterNativeView; 13 | 14 | public class StartRecordingAppWidgetProvider extends AppWidgetProvider { 15 | private static final String CHANNEL = "rhasspy_mobile_app/widget"; 16 | private static MethodChannel channel = null; 17 | private static FlutterNativeView backgroundFlutterView = null; 18 | 19 | 20 | @Override 21 | public void onEnabled(Context context) { 22 | Log.i("HomeScreenWidget", "onEnabled!"); 23 | } 24 | @Override 25 | public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) { 26 | for (int appWidgetId : appWidgetIds){ 27 | Intent intent = new Intent(context, MainActivity.class).addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); 28 | intent.putExtra("StartRecording",""); 29 | PendingIntent pendingIntent = PendingIntent.getActivity(context,0,intent,PendingIntent.FLAG_UPDATE_CURRENT); 30 | RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.start_recording_widget); 31 | views.setOnClickPendingIntent(R.id.start_recording_widget_button, pendingIntent); 32 | appWidgetManager.updateAppWidget(appWidgetId, views); 33 | Log.i("HomeScreenWidget", "onUpdate!"); 34 | 35 | 36 | } 37 | super.onUpdate(context, appWidgetManager, appWidgetIds); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /android/app/src/main/java/com/example/rhasspy_mobile_app/WakeWordService.java: -------------------------------------------------------------------------------- 1 | package com.example.rhasspy_mobile_app; 2 | 3 | import android.app.Notification; 4 | import android.app.NotificationChannel; 5 | import android.app.NotificationManager; 6 | import android.app.PendingIntent; 7 | import android.app.Service; 8 | import android.content.Intent; 9 | import android.media.AudioFormat; 10 | import android.media.AudioRecord; 11 | import android.media.MediaRecorder; 12 | import android.os.Binder; 13 | import android.os.Build; 14 | import android.os.IBinder; 15 | import android.util.Log; 16 | 17 | import androidx.annotation.Nullable; 18 | import androidx.core.app.NotificationCompat; 19 | 20 | import java.io.ByteArrayOutputStream; 21 | import java.net.DatagramPacket; 22 | import java.net.DatagramSocket; 23 | import java.net.InetAddress; 24 | import java.net.SocketException; 25 | import java.net.UnknownHostException; 26 | 27 | public class WakeWordService extends Service { 28 | 29 | public class LocalBinder extends Binder { 30 | WakeWordService getService() { 31 | return WakeWordService.this; 32 | } 33 | } 34 | 35 | private AudioRecord recorder; 36 | private static int BUFFER_SIZE = 2048; 37 | private static long byteRate = 16 * 16000 * 1 / 8; 38 | private int sampleRate = 16000; 39 | public boolean isActive = false; 40 | private static String TAG = "WakeWord"; 41 | public boolean isPaused = false; 42 | private final IBinder binder = new LocalBinder(); 43 | private InetAddress local; 44 | private int port; 45 | private DatagramSocket dsocket; 46 | private String wakeWordDetector; 47 | 48 | @Nullable 49 | @Override 50 | public IBinder onBind(Intent intent) { 51 | return binder; 52 | } 53 | 54 | @Override 55 | public int onStartCommand(Intent intent, int flags, int startId) { 56 | if (intent.getAction() != null) { 57 | if (intent.getAction().equals("Stop")) { 58 | Log.i(TAG, "Received stop "); 59 | stopForeground(true); 60 | stopSelfResult(startId); 61 | isActive = false; 62 | return START_NOT_STICKY; 63 | } 64 | } 65 | wakeWordDetector = intent.getStringExtra("wakeWordDetector"); 66 | switch (wakeWordDetector) { 67 | case "UDP": 68 | try { 69 | dsocket = new DatagramSocket(); 70 | } catch (SocketException e) { 71 | e.printStackTrace(); 72 | } 73 | String ip = intent.getStringExtra("ip"); 74 | port = intent.getIntExtra("port", 12101); 75 | try { 76 | local = InetAddress.getByName(ip); 77 | } catch (UnknownHostException e) { 78 | e.printStackTrace(); 79 | } 80 | } 81 | 82 | 83 | createNotificationChannel(); 84 | Intent notificationIntent = new Intent(this, MainActivity.class); 85 | PendingIntent pendingIntent = PendingIntent.getActivity(this, 86 | 0, notificationIntent, 0); 87 | 88 | Notification notification = new NotificationCompat.Builder(this, "Wake word") 89 | .setContentTitle("Wake Word") 90 | .setContentText("Listening for Wake Word") 91 | .setSmallIcon(R.drawable.app_icon) 92 | .setContentIntent(pendingIntent) 93 | .build(); 94 | 95 | startForeground(1, notification); 96 | 97 | startRecorder(); 98 | 99 | 100 | return super.onStartCommand(intent, flags, startId); 101 | } 102 | 103 | 104 | public void startRecorder() { 105 | Log.i(TAG, "Starting listening"); 106 | isActive = true; 107 | startStreaming(); 108 | } 109 | 110 | public void stopService() { 111 | Log.i(TAG, "Stopping the service"); 112 | isActive = false; 113 | recorder.release(); 114 | stopForeground(true); 115 | stopSelf(); 116 | } 117 | 118 | public void pause() { 119 | Log.i(TAG, "pause the audio stream"); 120 | isPaused = true; 121 | if (recorder != null) recorder.stop(); 122 | } 123 | 124 | public void resume() { 125 | Log.i(TAG, "resume the audio stream"); 126 | isPaused = false; 127 | if (recorder != null) recorder.startRecording(); 128 | } 129 | 130 | private void createNotificationChannel() { 131 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { 132 | NotificationChannel serviceChannel = new NotificationChannel( 133 | "Wake word", 134 | "Wake word", 135 | NotificationManager.IMPORTANCE_DEFAULT 136 | ); 137 | 138 | NotificationManager manager = getSystemService(NotificationManager.class); 139 | manager.createNotificationChannel(serviceChannel); 140 | } 141 | } 142 | 143 | @Override 144 | public boolean onUnbind(Intent intent) { 145 | Log.i(TAG, "unbind"); 146 | return super.onUnbind(intent); 147 | 148 | 149 | } 150 | 151 | private byte[] WaveHeader(long totalAudioLen, 152 | long longSampleRate, int channels, long byteRate) { 153 | long totalDataLen = totalAudioLen + 36; 154 | byte[] header = new byte[44]; 155 | header[0] = 'R'; // RIFF/WAVE header 156 | header[1] = 'I'; 157 | header[2] = 'F'; 158 | header[3] = 'F'; 159 | header[4] = (byte) (totalDataLen & 0xff); 160 | header[5] = (byte) ((totalDataLen >> 8) & 0xff); 161 | header[6] = (byte) ((totalDataLen >> 16) & 0xff); 162 | header[7] = (byte) ((totalDataLen >> 24) & 0xff); 163 | header[8] = 'W'; 164 | header[9] = 'A'; 165 | header[10] = 'V'; 166 | header[11] = 'E'; 167 | header[12] = 'f'; // 'fmt ' chunk 168 | header[13] = 'm'; 169 | header[14] = 't'; 170 | header[15] = ' '; 171 | header[16] = 16; // 4 bytes: size of 'fmt ' chunk 172 | header[17] = 0; 173 | header[18] = 0; 174 | header[19] = 0; 175 | header[20] = 1; // format = 1 176 | header[21] = 0; 177 | header[22] = (byte) channels; 178 | header[23] = 0; 179 | header[24] = (byte) (longSampleRate & 0xff); 180 | header[25] = (byte) ((longSampleRate >> 8) & 0xff); 181 | header[26] = (byte) ((longSampleRate >> 16) & 0xff); 182 | header[27] = (byte) ((longSampleRate >> 24) & 0xff); 183 | header[28] = (byte) (byteRate & 0xff); 184 | header[29] = (byte) ((byteRate >> 8) & 0xff); 185 | header[30] = (byte) ((byteRate >> 16) & 0xff); 186 | header[31] = (byte) ((byteRate >> 24) & 0xff); 187 | header[32] = (byte) (1); // block align 188 | header[33] = 0; 189 | header[34] = 16; // bits per sample 190 | header[35] = 0; 191 | header[36] = 'd'; 192 | header[37] = 'a'; 193 | header[38] = 't'; 194 | header[39] = 'a'; 195 | header[40] = (byte) (totalAudioLen & 0xff); 196 | header[41] = (byte) ((totalAudioLen >> 8) & 0xff); 197 | header[42] = (byte) ((totalAudioLen >> 16) & 0xff); 198 | header[43] = (byte) ((totalAudioLen >> 24) & 0xff); 199 | return header; 200 | 201 | } 202 | 203 | private void startStreaming() { 204 | 205 | Thread streamThread = new Thread(() -> { 206 | try { 207 | int bufferSize = BUFFER_SIZE; 208 | byte[] buffer = new byte[bufferSize]; 209 | 210 | android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_AUDIO); 211 | 212 | recorder = new AudioRecord(MediaRecorder.AudioSource.MIC, sampleRate, AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT, bufferSize); 213 | 214 | Log.d(TAG, "start recording"); 215 | recorder.startRecording(); 216 | 217 | while (isActive) { 218 | if (isPaused) continue; 219 | 220 | int result = recorder.read(buffer, 0, buffer.length); 221 | if (result == AudioRecord.ERROR_BAD_VALUE || result == AudioRecord.ERROR_DEAD_OBJECT 222 | || result == AudioRecord.ERROR_INVALID_OPERATION || result == AudioRecord.ERROR) { 223 | recorder.stop(); 224 | recorder.release(); 225 | recorder = new AudioRecord(MediaRecorder.AudioSource.MIC, sampleRate, AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT, bufferSize); 226 | recorder.startRecording(); 227 | continue; 228 | 229 | } 230 | if (result == 0) { 231 | Log.i(TAG, "Silence receiving"); 232 | recorder.stop(); 233 | recorder.startRecording(); 234 | continue; 235 | 236 | } 237 | 238 | ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); 239 | outputStream.write(WaveHeader(buffer.length, sampleRate, 1, byteRate)); 240 | outputStream.write(buffer); 241 | 242 | switch (wakeWordDetector) { 243 | case "UDP": 244 | try { 245 | DatagramPacket p = new DatagramPacket(outputStream.toByteArray(), outputStream.size(), local, port); 246 | 247 | dsocket.send(p); 248 | 249 | } catch (Exception e) { 250 | Log.e(TAG, "Exception: " + e); 251 | } 252 | } 253 | 254 | } 255 | 256 | Log.d(TAG, "AudioRecord finished recording"); 257 | } catch (Exception e) { 258 | Log.e(TAG, "Exception: " + e); 259 | } 260 | }); 261 | 262 | // start the thread 263 | streamThread.start(); 264 | } 265 | 266 | @Override 267 | public void onDestroy() { 268 | Log.i(TAG, "Destroying"); 269 | recorder.stop(); 270 | recorder.release(); 271 | isActive = false; 272 | recorder = null; 273 | dsocket.close(); 274 | super.onDestroy(); 275 | } 276 | } 277 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/app_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/razzo04/rhasspy-mobile-app/3c59971270eab0278cd5dbf6adac4064b5f14908/android/app/src/main/res/drawable/app_icon.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /android/app/src/main/res/layout/start_recording_widget.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 12 | 13 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/razzo04/rhasspy-mobile-app/3c59971270eab0278cd5dbf6adac4064b5f14908/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/razzo04/rhasspy-mobile-app/3c59971270eab0278cd5dbf6adac4064b5f14908/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/razzo04/rhasspy-mobile-app/3c59971270eab0278cd5dbf6adac4064b5f14908/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/razzo04/rhasspy-mobile-app/3c59971270eab0278cd5dbf6adac4064b5f14908/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/razzo04/rhasspy-mobile-app/3c59971270eab0278cd5dbf6adac4064b5f14908/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /android/app/src/main/res/xml/start_recoding_appwidget_info.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /android/app/src/profile/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | repositories { 3 | google() 4 | jcenter() 5 | } 6 | 7 | dependencies { 8 | classpath 'com.android.tools.build:gradle:4.0.2' 9 | } 10 | } 11 | 12 | allprojects { 13 | repositories { 14 | google() 15 | jcenter() 16 | } 17 | } 18 | 19 | rootProject.buildDir = '../build' 20 | subprojects { 21 | project.buildDir = "${rootProject.buildDir}/${project.name}" 22 | } 23 | subprojects { 24 | project.evaluationDependsOn(':app') 25 | } 26 | 27 | task clean(type: Delete) { 28 | delete rootProject.buildDir 29 | } 30 | -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536M 2 | android.useAndroidX=true 3 | android.enableJetifier=true 4 | android.enableR8=true 5 | -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Sat Aug 29 13:02:54 CEST 2020 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.1.1-all.zip 7 | -------------------------------------------------------------------------------- /android/settings.gradle: -------------------------------------------------------------------------------- 1 | // Copyright 2014 The Flutter Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | include ':app' 6 | 7 | def localPropertiesFile = new File(rootProject.projectDir, "local.properties") 8 | def properties = new Properties() 9 | 10 | assert localPropertiesFile.exists() 11 | localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } 12 | 13 | def flutterSdkPath = properties.getProperty("flutter.sdk") 14 | assert flutterSdkPath != null, "flutter.sdk not set in local.properties" 15 | apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" 16 | -------------------------------------------------------------------------------- /android/settings_aar.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | -------------------------------------------------------------------------------- /assets/images/rhasspy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/razzo04/rhasspy-mobile-app/3c59971270eab0278cd5dbf6adac4064b5f14908/assets/images/rhasspy.png -------------------------------------------------------------------------------- /ios/.gitignore: -------------------------------------------------------------------------------- 1 | *.mode1v3 2 | *.mode2v3 3 | *.moved-aside 4 | *.pbxuser 5 | *.perspectivev3 6 | **/*sync/ 7 | .sconsign.dblite 8 | .tags* 9 | **/.vagrant/ 10 | **/DerivedData/ 11 | Icon? 12 | **/Pods/ 13 | **/.symlinks/ 14 | profile 15 | xcuserdata 16 | **/.generated/ 17 | Flutter/App.framework 18 | Flutter/Flutter.framework 19 | Flutter/Flutter.podspec 20 | Flutter/Generated.xcconfig 21 | Flutter/app.flx 22 | Flutter/app.zip 23 | Flutter/flutter_assets/ 24 | Flutter/flutter_export_environment.sh 25 | ServiceDefinitions.json 26 | Runner/GeneratedPluginRegistrant.* 27 | 28 | # Exceptions to above rules. 29 | !default.mode1v3 30 | !default.mode2v3 31 | !default.pbxuser 32 | !default.perspectivev3 33 | -------------------------------------------------------------------------------- /ios/Flutter/AppFrameworkInfo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | App 9 | CFBundleIdentifier 10 | io.flutter.flutter.app 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | App 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1.0 23 | MinimumOSVersion 24 | 8.0 25 | 26 | 27 | -------------------------------------------------------------------------------- /ios/Flutter/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Generated.xcconfig" 2 | -------------------------------------------------------------------------------- /ios/Flutter/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Generated.xcconfig" 2 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; 11 | 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 12 | 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; 13 | 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 14 | 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 15 | 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; 16 | /* End PBXBuildFile section */ 17 | 18 | /* Begin PBXCopyFilesBuildPhase section */ 19 | 9705A1C41CF9048500538489 /* Embed Frameworks */ = { 20 | isa = PBXCopyFilesBuildPhase; 21 | buildActionMask = 2147483647; 22 | dstPath = ""; 23 | dstSubfolderSpec = 10; 24 | files = ( 25 | ); 26 | name = "Embed Frameworks"; 27 | runOnlyForDeploymentPostprocessing = 0; 28 | }; 29 | /* End PBXCopyFilesBuildPhase section */ 30 | 31 | /* Begin PBXFileReference section */ 32 | 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 33 | 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; 34 | 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; 35 | 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; 36 | 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 37 | 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; 38 | 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; 39 | 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; 40 | 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; 41 | 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 42 | 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 43 | 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 44 | 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 45 | /* End PBXFileReference section */ 46 | 47 | /* Begin PBXFrameworksBuildPhase section */ 48 | 97C146EB1CF9000F007C117D /* Frameworks */ = { 49 | isa = PBXFrameworksBuildPhase; 50 | buildActionMask = 2147483647; 51 | files = ( 52 | ); 53 | runOnlyForDeploymentPostprocessing = 0; 54 | }; 55 | /* End PBXFrameworksBuildPhase section */ 56 | 57 | /* Begin PBXGroup section */ 58 | 9740EEB11CF90186004384FC /* Flutter */ = { 59 | isa = PBXGroup; 60 | children = ( 61 | 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, 62 | 9740EEB21CF90195004384FC /* Debug.xcconfig */, 63 | 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, 64 | 9740EEB31CF90195004384FC /* Generated.xcconfig */, 65 | ); 66 | name = Flutter; 67 | sourceTree = ""; 68 | }; 69 | 97C146E51CF9000F007C117D = { 70 | isa = PBXGroup; 71 | children = ( 72 | 9740EEB11CF90186004384FC /* Flutter */, 73 | 97C146F01CF9000F007C117D /* Runner */, 74 | 97C146EF1CF9000F007C117D /* Products */, 75 | ); 76 | sourceTree = ""; 77 | }; 78 | 97C146EF1CF9000F007C117D /* Products */ = { 79 | isa = PBXGroup; 80 | children = ( 81 | 97C146EE1CF9000F007C117D /* Runner.app */, 82 | ); 83 | name = Products; 84 | sourceTree = ""; 85 | }; 86 | 97C146F01CF9000F007C117D /* Runner */ = { 87 | isa = PBXGroup; 88 | children = ( 89 | 97C146FA1CF9000F007C117D /* Main.storyboard */, 90 | 97C146FD1CF9000F007C117D /* Assets.xcassets */, 91 | 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, 92 | 97C147021CF9000F007C117D /* Info.plist */, 93 | 97C146F11CF9000F007C117D /* Supporting Files */, 94 | 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, 95 | 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, 96 | 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, 97 | 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, 98 | ); 99 | path = Runner; 100 | sourceTree = ""; 101 | }; 102 | 97C146F11CF9000F007C117D /* Supporting Files */ = { 103 | isa = PBXGroup; 104 | children = ( 105 | ); 106 | name = "Supporting Files"; 107 | sourceTree = ""; 108 | }; 109 | /* End PBXGroup section */ 110 | 111 | /* Begin PBXNativeTarget section */ 112 | 97C146ED1CF9000F007C117D /* Runner */ = { 113 | isa = PBXNativeTarget; 114 | buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; 115 | buildPhases = ( 116 | 9740EEB61CF901F6004384FC /* Run Script */, 117 | 97C146EA1CF9000F007C117D /* Sources */, 118 | 97C146EB1CF9000F007C117D /* Frameworks */, 119 | 97C146EC1CF9000F007C117D /* Resources */, 120 | 9705A1C41CF9048500538489 /* Embed Frameworks */, 121 | 3B06AD1E1E4923F5004D2608 /* Thin Binary */, 122 | ); 123 | buildRules = ( 124 | ); 125 | dependencies = ( 126 | ); 127 | name = Runner; 128 | productName = Runner; 129 | productReference = 97C146EE1CF9000F007C117D /* Runner.app */; 130 | productType = "com.apple.product-type.application"; 131 | }; 132 | /* End PBXNativeTarget section */ 133 | 134 | /* Begin PBXProject section */ 135 | 97C146E61CF9000F007C117D /* Project object */ = { 136 | isa = PBXProject; 137 | attributes = { 138 | LastUpgradeCheck = 1020; 139 | ORGANIZATIONNAME = ""; 140 | TargetAttributes = { 141 | 97C146ED1CF9000F007C117D = { 142 | CreatedOnToolsVersion = 7.3.1; 143 | LastSwiftMigration = 1100; 144 | }; 145 | }; 146 | }; 147 | buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; 148 | compatibilityVersion = "Xcode 9.3"; 149 | developmentRegion = en; 150 | hasScannedForEncodings = 0; 151 | knownRegions = ( 152 | en, 153 | Base, 154 | ); 155 | mainGroup = 97C146E51CF9000F007C117D; 156 | productRefGroup = 97C146EF1CF9000F007C117D /* Products */; 157 | projectDirPath = ""; 158 | projectRoot = ""; 159 | targets = ( 160 | 97C146ED1CF9000F007C117D /* Runner */, 161 | ); 162 | }; 163 | /* End PBXProject section */ 164 | 165 | /* Begin PBXResourcesBuildPhase section */ 166 | 97C146EC1CF9000F007C117D /* Resources */ = { 167 | isa = PBXResourcesBuildPhase; 168 | buildActionMask = 2147483647; 169 | files = ( 170 | 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, 171 | 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, 172 | 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, 173 | 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, 174 | ); 175 | runOnlyForDeploymentPostprocessing = 0; 176 | }; 177 | /* End PBXResourcesBuildPhase section */ 178 | 179 | /* Begin PBXShellScriptBuildPhase section */ 180 | 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { 181 | isa = PBXShellScriptBuildPhase; 182 | buildActionMask = 2147483647; 183 | files = ( 184 | ); 185 | inputPaths = ( 186 | ); 187 | name = "Thin Binary"; 188 | outputPaths = ( 189 | ); 190 | runOnlyForDeploymentPostprocessing = 0; 191 | shellPath = /bin/sh; 192 | shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; 193 | }; 194 | 9740EEB61CF901F6004384FC /* Run Script */ = { 195 | isa = PBXShellScriptBuildPhase; 196 | buildActionMask = 2147483647; 197 | files = ( 198 | ); 199 | inputPaths = ( 200 | ); 201 | name = "Run Script"; 202 | outputPaths = ( 203 | ); 204 | runOnlyForDeploymentPostprocessing = 0; 205 | shellPath = /bin/sh; 206 | shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; 207 | }; 208 | /* End PBXShellScriptBuildPhase section */ 209 | 210 | /* Begin PBXSourcesBuildPhase section */ 211 | 97C146EA1CF9000F007C117D /* Sources */ = { 212 | isa = PBXSourcesBuildPhase; 213 | buildActionMask = 2147483647; 214 | files = ( 215 | 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, 216 | 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, 217 | ); 218 | runOnlyForDeploymentPostprocessing = 0; 219 | }; 220 | /* End PBXSourcesBuildPhase section */ 221 | 222 | /* Begin PBXVariantGroup section */ 223 | 97C146FA1CF9000F007C117D /* Main.storyboard */ = { 224 | isa = PBXVariantGroup; 225 | children = ( 226 | 97C146FB1CF9000F007C117D /* Base */, 227 | ); 228 | name = Main.storyboard; 229 | sourceTree = ""; 230 | }; 231 | 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { 232 | isa = PBXVariantGroup; 233 | children = ( 234 | 97C147001CF9000F007C117D /* Base */, 235 | ); 236 | name = LaunchScreen.storyboard; 237 | sourceTree = ""; 238 | }; 239 | /* End PBXVariantGroup section */ 240 | 241 | /* Begin XCBuildConfiguration section */ 242 | 249021D3217E4FDB00AE95B9 /* Profile */ = { 243 | isa = XCBuildConfiguration; 244 | baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; 245 | buildSettings = { 246 | ALWAYS_SEARCH_USER_PATHS = NO; 247 | CLANG_ANALYZER_NONNULL = YES; 248 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 249 | CLANG_CXX_LIBRARY = "libc++"; 250 | CLANG_ENABLE_MODULES = YES; 251 | CLANG_ENABLE_OBJC_ARC = YES; 252 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 253 | CLANG_WARN_BOOL_CONVERSION = YES; 254 | CLANG_WARN_COMMA = YES; 255 | CLANG_WARN_CONSTANT_CONVERSION = YES; 256 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 257 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 258 | CLANG_WARN_EMPTY_BODY = YES; 259 | CLANG_WARN_ENUM_CONVERSION = YES; 260 | CLANG_WARN_INFINITE_RECURSION = YES; 261 | CLANG_WARN_INT_CONVERSION = YES; 262 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 263 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 264 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 265 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 266 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 267 | CLANG_WARN_STRICT_PROTOTYPES = YES; 268 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 269 | CLANG_WARN_UNREACHABLE_CODE = YES; 270 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 271 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 272 | COPY_PHASE_STRIP = NO; 273 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 274 | ENABLE_NS_ASSERTIONS = NO; 275 | ENABLE_STRICT_OBJC_MSGSEND = YES; 276 | GCC_C_LANGUAGE_STANDARD = gnu99; 277 | GCC_NO_COMMON_BLOCKS = YES; 278 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 279 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 280 | GCC_WARN_UNDECLARED_SELECTOR = YES; 281 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 282 | GCC_WARN_UNUSED_FUNCTION = YES; 283 | GCC_WARN_UNUSED_VARIABLE = YES; 284 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 285 | MTL_ENABLE_DEBUG_INFO = NO; 286 | SDKROOT = iphoneos; 287 | SUPPORTED_PLATFORMS = iphoneos; 288 | TARGETED_DEVICE_FAMILY = "1,2"; 289 | VALIDATE_PRODUCT = YES; 290 | }; 291 | name = Profile; 292 | }; 293 | 249021D4217E4FDB00AE95B9 /* Profile */ = { 294 | isa = XCBuildConfiguration; 295 | baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; 296 | buildSettings = { 297 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 298 | CLANG_ENABLE_MODULES = YES; 299 | CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; 300 | ENABLE_BITCODE = NO; 301 | FRAMEWORK_SEARCH_PATHS = ( 302 | "$(inherited)", 303 | "$(PROJECT_DIR)/Flutter", 304 | ); 305 | INFOPLIST_FILE = Runner/Info.plist; 306 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 307 | LIBRARY_SEARCH_PATHS = ( 308 | "$(inherited)", 309 | "$(PROJECT_DIR)/Flutter", 310 | ); 311 | PRODUCT_BUNDLE_IDENTIFIER = com.example.rhasspyMobileApp; 312 | PRODUCT_NAME = "$(TARGET_NAME)"; 313 | SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; 314 | SWIFT_VERSION = 5.0; 315 | VERSIONING_SYSTEM = "apple-generic"; 316 | }; 317 | name = Profile; 318 | }; 319 | 97C147031CF9000F007C117D /* Debug */ = { 320 | isa = XCBuildConfiguration; 321 | baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; 322 | buildSettings = { 323 | ALWAYS_SEARCH_USER_PATHS = NO; 324 | CLANG_ANALYZER_NONNULL = YES; 325 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 326 | CLANG_CXX_LIBRARY = "libc++"; 327 | CLANG_ENABLE_MODULES = YES; 328 | CLANG_ENABLE_OBJC_ARC = YES; 329 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 330 | CLANG_WARN_BOOL_CONVERSION = YES; 331 | CLANG_WARN_COMMA = YES; 332 | CLANG_WARN_CONSTANT_CONVERSION = YES; 333 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 334 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 335 | CLANG_WARN_EMPTY_BODY = YES; 336 | CLANG_WARN_ENUM_CONVERSION = YES; 337 | CLANG_WARN_INFINITE_RECURSION = YES; 338 | CLANG_WARN_INT_CONVERSION = YES; 339 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 340 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 341 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 342 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 343 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 344 | CLANG_WARN_STRICT_PROTOTYPES = YES; 345 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 346 | CLANG_WARN_UNREACHABLE_CODE = YES; 347 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 348 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 349 | COPY_PHASE_STRIP = NO; 350 | DEBUG_INFORMATION_FORMAT = dwarf; 351 | ENABLE_STRICT_OBJC_MSGSEND = YES; 352 | ENABLE_TESTABILITY = YES; 353 | GCC_C_LANGUAGE_STANDARD = gnu99; 354 | GCC_DYNAMIC_NO_PIC = NO; 355 | GCC_NO_COMMON_BLOCKS = YES; 356 | GCC_OPTIMIZATION_LEVEL = 0; 357 | GCC_PREPROCESSOR_DEFINITIONS = ( 358 | "DEBUG=1", 359 | "$(inherited)", 360 | ); 361 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 362 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 363 | GCC_WARN_UNDECLARED_SELECTOR = YES; 364 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 365 | GCC_WARN_UNUSED_FUNCTION = YES; 366 | GCC_WARN_UNUSED_VARIABLE = YES; 367 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 368 | MTL_ENABLE_DEBUG_INFO = YES; 369 | ONLY_ACTIVE_ARCH = YES; 370 | SDKROOT = iphoneos; 371 | TARGETED_DEVICE_FAMILY = "1,2"; 372 | }; 373 | name = Debug; 374 | }; 375 | 97C147041CF9000F007C117D /* Release */ = { 376 | isa = XCBuildConfiguration; 377 | baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; 378 | buildSettings = { 379 | ALWAYS_SEARCH_USER_PATHS = NO; 380 | CLANG_ANALYZER_NONNULL = YES; 381 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 382 | CLANG_CXX_LIBRARY = "libc++"; 383 | CLANG_ENABLE_MODULES = YES; 384 | CLANG_ENABLE_OBJC_ARC = YES; 385 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 386 | CLANG_WARN_BOOL_CONVERSION = YES; 387 | CLANG_WARN_COMMA = YES; 388 | CLANG_WARN_CONSTANT_CONVERSION = YES; 389 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 390 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 391 | CLANG_WARN_EMPTY_BODY = YES; 392 | CLANG_WARN_ENUM_CONVERSION = YES; 393 | CLANG_WARN_INFINITE_RECURSION = YES; 394 | CLANG_WARN_INT_CONVERSION = YES; 395 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 396 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 397 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 398 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 399 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 400 | CLANG_WARN_STRICT_PROTOTYPES = YES; 401 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 402 | CLANG_WARN_UNREACHABLE_CODE = YES; 403 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 404 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 405 | COPY_PHASE_STRIP = NO; 406 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 407 | ENABLE_NS_ASSERTIONS = NO; 408 | ENABLE_STRICT_OBJC_MSGSEND = YES; 409 | GCC_C_LANGUAGE_STANDARD = gnu99; 410 | GCC_NO_COMMON_BLOCKS = YES; 411 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 412 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 413 | GCC_WARN_UNDECLARED_SELECTOR = YES; 414 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 415 | GCC_WARN_UNUSED_FUNCTION = YES; 416 | GCC_WARN_UNUSED_VARIABLE = YES; 417 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 418 | MTL_ENABLE_DEBUG_INFO = NO; 419 | SDKROOT = iphoneos; 420 | SUPPORTED_PLATFORMS = iphoneos; 421 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 422 | TARGETED_DEVICE_FAMILY = "1,2"; 423 | VALIDATE_PRODUCT = YES; 424 | }; 425 | name = Release; 426 | }; 427 | 97C147061CF9000F007C117D /* Debug */ = { 428 | isa = XCBuildConfiguration; 429 | baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; 430 | buildSettings = { 431 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 432 | CLANG_ENABLE_MODULES = YES; 433 | CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; 434 | ENABLE_BITCODE = NO; 435 | FRAMEWORK_SEARCH_PATHS = ( 436 | "$(inherited)", 437 | "$(PROJECT_DIR)/Flutter", 438 | ); 439 | INFOPLIST_FILE = Runner/Info.plist; 440 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 441 | LIBRARY_SEARCH_PATHS = ( 442 | "$(inherited)", 443 | "$(PROJECT_DIR)/Flutter", 444 | ); 445 | PRODUCT_BUNDLE_IDENTIFIER = com.example.rhasspyMobileApp; 446 | PRODUCT_NAME = "$(TARGET_NAME)"; 447 | SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; 448 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 449 | SWIFT_VERSION = 5.0; 450 | VERSIONING_SYSTEM = "apple-generic"; 451 | }; 452 | name = Debug; 453 | }; 454 | 97C147071CF9000F007C117D /* Release */ = { 455 | isa = XCBuildConfiguration; 456 | baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; 457 | buildSettings = { 458 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 459 | CLANG_ENABLE_MODULES = YES; 460 | CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; 461 | ENABLE_BITCODE = NO; 462 | FRAMEWORK_SEARCH_PATHS = ( 463 | "$(inherited)", 464 | "$(PROJECT_DIR)/Flutter", 465 | ); 466 | INFOPLIST_FILE = Runner/Info.plist; 467 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 468 | LIBRARY_SEARCH_PATHS = ( 469 | "$(inherited)", 470 | "$(PROJECT_DIR)/Flutter", 471 | ); 472 | PRODUCT_BUNDLE_IDENTIFIER = com.example.rhasspyMobileApp; 473 | PRODUCT_NAME = "$(TARGET_NAME)"; 474 | SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; 475 | SWIFT_VERSION = 5.0; 476 | VERSIONING_SYSTEM = "apple-generic"; 477 | }; 478 | name = Release; 479 | }; 480 | /* End XCBuildConfiguration section */ 481 | 482 | /* Begin XCConfigurationList section */ 483 | 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { 484 | isa = XCConfigurationList; 485 | buildConfigurations = ( 486 | 97C147031CF9000F007C117D /* Debug */, 487 | 97C147041CF9000F007C117D /* Release */, 488 | 249021D3217E4FDB00AE95B9 /* Profile */, 489 | ); 490 | defaultConfigurationIsVisible = 0; 491 | defaultConfigurationName = Release; 492 | }; 493 | 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { 494 | isa = XCConfigurationList; 495 | buildConfigurations = ( 496 | 97C147061CF9000F007C117D /* Debug */, 497 | 97C147071CF9000F007C117D /* Release */, 498 | 249021D4217E4FDB00AE95B9 /* Profile */, 499 | ); 500 | defaultConfigurationIsVisible = 0; 501 | defaultConfigurationName = Release; 502 | }; 503 | /* End XCConfigurationList section */ 504 | }; 505 | rootObject = 97C146E61CF9000F007C117D /* Project object */; 506 | } 507 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 39 | 40 | 41 | 42 | 43 | 44 | 54 | 56 | 62 | 63 | 64 | 65 | 66 | 67 | 73 | 75 | 81 | 82 | 83 | 84 | 86 | 87 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import Flutter 3 | 4 | @UIApplicationMain 5 | @objc class AppDelegate: FlutterAppDelegate { 6 | override func application( 7 | _ application: UIApplication, 8 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? 9 | ) -> Bool { 10 | GeneratedPluginRegistrant.register(with: self) 11 | return super.application(application, didFinishLaunchingWithOptions: launchOptions) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "20x20", 5 | "idiom" : "iphone", 6 | "filename" : "Icon-App-20x20@2x.png", 7 | "scale" : "2x" 8 | }, 9 | { 10 | "size" : "20x20", 11 | "idiom" : "iphone", 12 | "filename" : "Icon-App-20x20@3x.png", 13 | "scale" : "3x" 14 | }, 15 | { 16 | "size" : "29x29", 17 | "idiom" : "iphone", 18 | "filename" : "Icon-App-29x29@1x.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "29x29", 23 | "idiom" : "iphone", 24 | "filename" : "Icon-App-29x29@2x.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "29x29", 29 | "idiom" : "iphone", 30 | "filename" : "Icon-App-29x29@3x.png", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "size" : "40x40", 35 | "idiom" : "iphone", 36 | "filename" : "Icon-App-40x40@2x.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "40x40", 41 | "idiom" : "iphone", 42 | "filename" : "Icon-App-40x40@3x.png", 43 | "scale" : "3x" 44 | }, 45 | { 46 | "size" : "60x60", 47 | "idiom" : "iphone", 48 | "filename" : "Icon-App-60x60@2x.png", 49 | "scale" : "2x" 50 | }, 51 | { 52 | "size" : "60x60", 53 | "idiom" : "iphone", 54 | "filename" : "Icon-App-60x60@3x.png", 55 | "scale" : "3x" 56 | }, 57 | { 58 | "size" : "20x20", 59 | "idiom" : "ipad", 60 | "filename" : "Icon-App-20x20@1x.png", 61 | "scale" : "1x" 62 | }, 63 | { 64 | "size" : "20x20", 65 | "idiom" : "ipad", 66 | "filename" : "Icon-App-20x20@2x.png", 67 | "scale" : "2x" 68 | }, 69 | { 70 | "size" : "29x29", 71 | "idiom" : "ipad", 72 | "filename" : "Icon-App-29x29@1x.png", 73 | "scale" : "1x" 74 | }, 75 | { 76 | "size" : "29x29", 77 | "idiom" : "ipad", 78 | "filename" : "Icon-App-29x29@2x.png", 79 | "scale" : "2x" 80 | }, 81 | { 82 | "size" : "40x40", 83 | "idiom" : "ipad", 84 | "filename" : "Icon-App-40x40@1x.png", 85 | "scale" : "1x" 86 | }, 87 | { 88 | "size" : "40x40", 89 | "idiom" : "ipad", 90 | "filename" : "Icon-App-40x40@2x.png", 91 | "scale" : "2x" 92 | }, 93 | { 94 | "size" : "76x76", 95 | "idiom" : "ipad", 96 | "filename" : "Icon-App-76x76@1x.png", 97 | "scale" : "1x" 98 | }, 99 | { 100 | "size" : "76x76", 101 | "idiom" : "ipad", 102 | "filename" : "Icon-App-76x76@2x.png", 103 | "scale" : "2x" 104 | }, 105 | { 106 | "size" : "83.5x83.5", 107 | "idiom" : "ipad", 108 | "filename" : "Icon-App-83.5x83.5@2x.png", 109 | "scale" : "2x" 110 | }, 111 | { 112 | "size" : "1024x1024", 113 | "idiom" : "ios-marketing", 114 | "filename" : "Icon-App-1024x1024@1x.png", 115 | "scale" : "1x" 116 | } 117 | ], 118 | "info" : { 119 | "version" : 1, 120 | "author" : "xcode" 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/razzo04/rhasspy-mobile-app/3c59971270eab0278cd5dbf6adac4064b5f14908/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/razzo04/rhasspy-mobile-app/3c59971270eab0278cd5dbf6adac4064b5f14908/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/razzo04/rhasspy-mobile-app/3c59971270eab0278cd5dbf6adac4064b5f14908/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/razzo04/rhasspy-mobile-app/3c59971270eab0278cd5dbf6adac4064b5f14908/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/razzo04/rhasspy-mobile-app/3c59971270eab0278cd5dbf6adac4064b5f14908/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/razzo04/rhasspy-mobile-app/3c59971270eab0278cd5dbf6adac4064b5f14908/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/razzo04/rhasspy-mobile-app/3c59971270eab0278cd5dbf6adac4064b5f14908/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/razzo04/rhasspy-mobile-app/3c59971270eab0278cd5dbf6adac4064b5f14908/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/razzo04/rhasspy-mobile-app/3c59971270eab0278cd5dbf6adac4064b5f14908/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/razzo04/rhasspy-mobile-app/3c59971270eab0278cd5dbf6adac4064b5f14908/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/razzo04/rhasspy-mobile-app/3c59971270eab0278cd5dbf6adac4064b5f14908/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/razzo04/rhasspy-mobile-app/3c59971270eab0278cd5dbf6adac4064b5f14908/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/razzo04/rhasspy-mobile-app/3c59971270eab0278cd5dbf6adac4064b5f14908/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/razzo04/rhasspy-mobile-app/3c59971270eab0278cd5dbf6adac4064b5f14908/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/razzo04/rhasspy-mobile-app/3c59971270eab0278cd5dbf6adac4064b5f14908/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/razzo04/rhasspy-mobile-app/3c59971270eab0278cd5dbf6adac4064b5f14908/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/razzo04/rhasspy-mobile-app/3c59971270eab0278cd5dbf6adac4064b5f14908/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/razzo04/rhasspy-mobile-app/3c59971270eab0278cd5dbf6adac4064b5f14908/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/razzo04/rhasspy-mobile-app/3c59971270eab0278cd5dbf6adac4064b5f14908/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/razzo04/rhasspy-mobile-app/3c59971270eab0278cd5dbf6adac4064b5f14908/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/razzo04/rhasspy-mobile-app/3c59971270eab0278cd5dbf6adac4064b5f14908/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "LaunchImage.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "LaunchImage@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "LaunchImage@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/razzo04/rhasspy-mobile-app/3c59971270eab0278cd5dbf6adac4064b5f14908/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/razzo04/rhasspy-mobile-app/3c59971270eab0278cd5dbf6adac4064b5f14908/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/razzo04/rhasspy-mobile-app/3c59971270eab0278cd5dbf6adac4064b5f14908/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md: -------------------------------------------------------------------------------- 1 | # Launch Screen Assets 2 | 3 | You can customize the launch screen with your own desired assets by replacing the image files in this directory. 4 | 5 | You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. -------------------------------------------------------------------------------- /ios/Runner/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /ios/Runner/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /ios/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | rhasspy_mobile_app 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | $(FLUTTER_BUILD_NAME) 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | $(FLUTTER_BUILD_NUMBER) 23 | LSRequiresIPhoneOS 24 | 25 | UILaunchStoryboardName 26 | LaunchScreen 27 | UIMainStoryboardFile 28 | Main 29 | UISupportedInterfaceOrientations 30 | 31 | UIInterfaceOrientationPortrait 32 | UIInterfaceOrientationLandscapeLeft 33 | UIInterfaceOrientationLandscapeRight 34 | 35 | UISupportedInterfaceOrientations~ipad 36 | 37 | UIInterfaceOrientationPortrait 38 | UIInterfaceOrientationPortraitUpsideDown 39 | UIInterfaceOrientationLandscapeLeft 40 | UIInterfaceOrientationLandscapeRight 41 | 42 | UIViewControllerBasedStatusBarAppearance 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /ios/Runner/Runner-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | #import "GeneratedPluginRegistrant.h" 2 | -------------------------------------------------------------------------------- /lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:io'; 3 | 4 | import 'package:flutter/foundation.dart'; 5 | import 'package:flutter/material.dart'; 6 | import 'package:rhasspy_mobile_app/utils/logger/logger.dart'; 7 | import 'package:path_provider/path_provider.dart'; 8 | import 'package:provider/provider.dart'; 9 | import 'package:rhasspy_mobile_app/screens/app_settings.dart'; 10 | import 'package:rhasspy_mobile_app/screens/home_page.dart'; 11 | import 'package:shared_preferences/shared_preferences.dart'; 12 | import 'package:rhasspy_mobile_app/utils/constants.dart'; 13 | import 'rhasspy_dart/rhasspy_mqtt_isolate.dart'; 14 | import 'utils/utils.dart'; 15 | 16 | RhasspyMqttIsolate rhasspyMqttIsolate; 17 | Logger log; 18 | void setupLogger() async { 19 | MemoryLogOutput memoryOutput = MemoryLogOutput(); 20 | WidgetsFlutterBinding.ensureInitialized(); 21 | File logFile; 22 | if (Platform.isAndroid) { 23 | logFile = File((await getExternalStorageDirectory()).path + "/logs.txt"); 24 | } else { 25 | logFile = 26 | File((await getApplicationDocumentsDirectory()).path + "/logs.txt"); 27 | } 28 | log = Logger( 29 | logOutput: MultiOutput([ 30 | memoryOutput, 31 | ConsoleOutput(printer: const SimplePrinter(includeStackTrace: false)), 32 | FileOutput( 33 | overrideExisting: true, 34 | file: logFile, 35 | ) 36 | ])); 37 | FlutterError.onError = (FlutterErrorDetails details) { 38 | if (!kReleaseMode) FlutterError.dumpErrorToConsole(details); 39 | log.log(Level.error, details.toString(), 40 | stackTrace: details.stack, 41 | tag: details.exceptionAsString(), 42 | includeTime: true); 43 | }; 44 | } 45 | 46 | Future setupMqtt() async { 47 | if (rhasspyMqttIsolate != null) return rhasspyMqttIsolate; 48 | String certificatePath; 49 | WidgetsFlutterBinding.ensureInitialized(); 50 | Directory appDocDirectory = await getApplicationDocumentsDirectory(); 51 | certificatePath = appDocDirectory.path + "/mqttCertificate.pem"; 52 | if (!File(certificatePath).existsSync()) { 53 | certificatePath = null; 54 | } 55 | SharedPreferences prefs = await SharedPreferences.getInstance(); 56 | log.d("Starting mqtt...", mqttTag); 57 | rhasspyMqttIsolate = RhasspyMqttIsolate( 58 | prefs.getString("MQTTHOST") ?? "", 59 | prefs.getInt("MQTTPORT") ?? 1883, 60 | prefs.getBool("MQTTSSL") ?? false, 61 | prefs.getString("MQTTUSERNAME") ?? "", 62 | prefs.getString("MQTTPASSWORD") ?? "", 63 | prefs.getString("SITEID") ?? "", 64 | pemFilePath: certificatePath, 65 | ); 66 | if (prefs.containsKey("MQTT") && prefs.getBool("MQTT")) { 67 | log.d("Connecting to mqtt...", mqttTag); 68 | rhasspyMqttIsolate.connect(); 69 | } 70 | return rhasspyMqttIsolate; 71 | } 72 | 73 | void main() { 74 | setupLogger(); 75 | runZonedGuarded>(() async { 76 | runApp(MyApp()); 77 | }, (Object error, StackTrace stackTrace) { 78 | log.log(Level.error, error.toString(), 79 | stackTrace: stackTrace, includeTime: true); 80 | }, zoneSpecification: ZoneSpecification(print: (self, parent, zone, message) { 81 | parent.print(zone, message); 82 | })); 83 | } 84 | 85 | class MyApp extends StatelessWidget { 86 | @override 87 | Widget build(BuildContext context) { 88 | return FutureProvider( 89 | create: (_) => setupMqtt(), 90 | child: MaterialApp( 91 | debugShowCheckedModeBanner: false, 92 | title: 'Rhasspy mobile app', 93 | onGenerateRoute: (RouteSettings settings) { 94 | var screen; 95 | switch (settings.name) { 96 | case HomePage.routeName: 97 | screen = HomePage(); 98 | break; 99 | case AppSettings.routeName: 100 | screen = AppSettings(); 101 | break; 102 | } 103 | return MaterialPageRoute( 104 | builder: (context) => screen, settings: settings); 105 | }, 106 | theme: ThemeData( 107 | primarySwatch: createMaterialColor(Color.fromARGB(255, 52, 58, 64)), 108 | visualDensity: VisualDensity.adaptivePlatformDensity, 109 | ), 110 | home: HomePage()), 111 | lazy: false, 112 | ); 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /lib/rhasspy_dart/exceptions.dart: -------------------------------------------------------------------------------- 1 | class NotConnected implements Exception {} 2 | -------------------------------------------------------------------------------- /lib/rhasspy_dart/parse_messages.dart: -------------------------------------------------------------------------------- 1 | class AsrTextCaptured { 2 | String text; 3 | double likelihood; 4 | double seconds; 5 | String siteId; 6 | String sessionId; 7 | String wakewordId; 8 | 9 | AsrTextCaptured( 10 | {this.text, 11 | this.likelihood, 12 | this.seconds, 13 | this.siteId, 14 | this.sessionId, 15 | this.wakewordId}); 16 | 17 | AsrTextCaptured.fromJson(Map json) { 18 | text = json['text']; 19 | if (json['likelihood'] is double) { 20 | likelihood = json['likelihood']; 21 | } else { 22 | likelihood = double.parse(json['likelihood'].toString()); 23 | } 24 | if (json['seconds'] is double) { 25 | seconds = json['seconds']; 26 | } else { 27 | seconds = double.parse(json['seconds'].toString()); 28 | } 29 | 30 | siteId = json['siteId']; 31 | sessionId = json['sessionId']; 32 | wakewordId = json['wakewordId']; 33 | } 34 | 35 | Map toJson() { 36 | final Map data = new Map(); 37 | data['text'] = this.text; 38 | data['likelihood'] = this.likelihood; 39 | data['seconds'] = this.seconds; 40 | data['siteId'] = this.siteId; 41 | data['sessionId'] = this.sessionId; 42 | data['wakewordId'] = this.wakewordId; 43 | return data; 44 | } 45 | } 46 | 47 | class NluIntentParsed { 48 | String input; 49 | Intent intent; 50 | String siteId; 51 | String id; 52 | List slots; 53 | String sessionId; 54 | 55 | NluIntentParsed( 56 | {this.input, 57 | this.intent, 58 | this.siteId, 59 | this.id, 60 | this.slots, 61 | this.sessionId}); 62 | 63 | NluIntentParsed.fromJson(Map json) { 64 | input = json['input']; 65 | intent = 66 | json['intent'] != null ? new Intent.fromJson(json['intent']) : null; 67 | siteId = json['siteId']; 68 | id = json['id']; 69 | if (json['slots'] != null) { 70 | slots = new List(); 71 | json['slots'].forEach((v) { 72 | slots.add(new Slots.fromJson(v)); 73 | }); 74 | } 75 | sessionId = json['sessionId']; 76 | } 77 | 78 | Map toJson() { 79 | final Map data = new Map(); 80 | data['input'] = this.input; 81 | if (this.intent != null) { 82 | data['intent'] = this.intent.toJson(); 83 | } 84 | data['siteId'] = this.siteId; 85 | data['id'] = this.id; 86 | if (this.slots != null) { 87 | data['slots'] = this.slots.map((v) => v.toJson()).toList(); 88 | } 89 | data['sessionId'] = this.sessionId; 90 | return data; 91 | } 92 | } 93 | 94 | class Intent { 95 | String intentName; 96 | double confidenceScore; 97 | 98 | Intent({this.intentName, this.confidenceScore}); 99 | 100 | Intent.fromJson(Map json) { 101 | intentName = json['intentName']; 102 | confidenceScore = json['confidenceScore']; 103 | } 104 | 105 | Map toJson() { 106 | final Map data = new Map(); 107 | data['intentName'] = this.intentName; 108 | data['confidenceScore'] = this.confidenceScore; 109 | return data; 110 | } 111 | } 112 | 113 | class Slots { 114 | String entity; 115 | Value value; 116 | String slotName; 117 | String rawValue; 118 | double confidence; 119 | Range range; 120 | 121 | Slots( 122 | {this.entity, 123 | this.value, 124 | this.slotName, 125 | this.rawValue, 126 | this.confidence, 127 | this.range}); 128 | 129 | Slots.fromJson(Map json) { 130 | entity = json['entity']; 131 | value = json['value'] != null ? new Value.fromJson(json['value']) : null; 132 | slotName = json['slotName']; 133 | rawValue = json['rawValue']; 134 | confidence = json['confidence']; 135 | range = json['range'] != null ? new Range.fromJson(json['range']) : null; 136 | } 137 | 138 | Map toJson() { 139 | final Map data = new Map(); 140 | data['entity'] = this.entity; 141 | if (this.value != null) { 142 | data['value'] = this.value.toJson(); 143 | } 144 | data['slotName'] = this.slotName; 145 | data['rawValue'] = this.rawValue; 146 | data['confidence'] = this.confidence; 147 | if (this.range != null) { 148 | data['range'] = this.range.toJson(); 149 | } 150 | return data; 151 | } 152 | } 153 | 154 | class Value { 155 | String kind; 156 | String value; 157 | 158 | Value({this.kind, this.value}); 159 | 160 | Value.fromJson(Map json) { 161 | kind = json['kind']; 162 | value = json['value'].toString(); 163 | } 164 | 165 | Map toJson() { 166 | final Map data = new Map(); 167 | data['kind'] = this.kind; 168 | data['value'] = this.value; 169 | return data; 170 | } 171 | } 172 | 173 | class Range { 174 | int start; 175 | int end; 176 | int rawStart; 177 | int rawEnd; 178 | 179 | Range({this.start, this.end, this.rawStart, this.rawEnd}); 180 | 181 | Range.fromJson(Map json) { 182 | start = json['start']; 183 | end = json['end']; 184 | rawStart = json['rawStart']; 185 | rawEnd = json['rawEnd']; 186 | } 187 | 188 | Map toJson() { 189 | final Map data = new Map(); 190 | data['start'] = this.start; 191 | data['end'] = this.end; 192 | data['rawStart'] = this.rawStart; 193 | data['rawEnd'] = this.rawEnd; 194 | return data; 195 | } 196 | } 197 | 198 | class DialogueEndSession { 199 | String sessionId; 200 | String text; 201 | String customData; 202 | 203 | DialogueEndSession({this.sessionId, this.text, this.customData}); 204 | 205 | DialogueEndSession.fromJson(Map json) { 206 | sessionId = json['sessionId']; 207 | text = json['text']; 208 | customData = json['customData']; 209 | } 210 | 211 | Map toJson() { 212 | final Map data = new Map(); 213 | data['sessionId'] = this.sessionId; 214 | data['text'] = this.text; 215 | data['customData'] = this.customData; 216 | return data; 217 | } 218 | } 219 | 220 | class DialogueContinueSession { 221 | String sessionId; 222 | String customData; 223 | String text; 224 | List intentFilter; 225 | bool sendIntentNotRecognized; 226 | List slot; 227 | String lang; 228 | 229 | DialogueContinueSession( 230 | {this.sessionId, 231 | this.customData, 232 | this.text, 233 | this.intentFilter, 234 | this.sendIntentNotRecognized, 235 | this.slot, 236 | this.lang}); 237 | 238 | DialogueContinueSession.fromJson(Map json) { 239 | sessionId = json['sessionId']; 240 | customData = json['customData']; 241 | text = json['text']; 242 | if (json['intentFilter'] != null) 243 | intentFilter = json['intentFilter'].cast(); 244 | sendIntentNotRecognized = json['sendIntentNotRecognized']; 245 | if (json['slots'] != null) { 246 | slot = new List(); 247 | json['slots'].forEach((v) { 248 | slot.add(new Slots.fromJson(v)); 249 | }); 250 | } 251 | lang = json['lang']; 252 | } 253 | 254 | Map toJson() { 255 | final Map data = new Map(); 256 | data['sessionId'] = this.sessionId; 257 | data['customData'] = this.customData; 258 | data['text'] = this.text; 259 | data['intentFilter'] = this.intentFilter; 260 | data['sendIntentNotRecognized'] = this.sendIntentNotRecognized; 261 | data['slot'] = this.slot; 262 | data['lang'] = this.lang; 263 | return data; 264 | } 265 | } 266 | 267 | class DialogueStartSession { 268 | String siteId; 269 | String customData; 270 | Init init; 271 | 272 | DialogueStartSession({this.siteId, this.customData, this.init}); 273 | 274 | DialogueStartSession.fromJson(Map json) { 275 | siteId = json['siteId']; 276 | customData = json['customData']; 277 | init = json['init'] != null ? new Init.fromJson(json['init']) : null; 278 | } 279 | 280 | Map toJson() { 281 | final Map data = new Map(); 282 | data['siteId'] = this.siteId; 283 | data['customData'] = this.customData; 284 | if (this.init != null) { 285 | data['init'] = this.init.toJson(); 286 | } 287 | return data; 288 | } 289 | } 290 | 291 | class Init { 292 | String type; 293 | String text; 294 | bool canBeEnqueued; 295 | List intentFilter; 296 | 297 | Init({this.type, this.text, this.canBeEnqueued, this.intentFilter}); 298 | 299 | Init.fromJson(Map json) { 300 | type = json['type']; 301 | text = json['text']; 302 | canBeEnqueued = json['canBeEnqueued']; 303 | if (intentFilter != null) 304 | intentFilter = json['intentFilter'].cast(); 305 | } 306 | 307 | Map toJson() { 308 | final Map data = new Map(); 309 | data['type'] = this.type; 310 | data['text'] = this.text; 311 | data['canBeEnqueued'] = this.canBeEnqueued; 312 | data['intentFilter'] = this.intentFilter; 313 | return data; 314 | } 315 | } 316 | 317 | class DialogueSessionStarted { 318 | String sessionId; 319 | String siteId; 320 | String customData; 321 | String lang; 322 | 323 | DialogueSessionStarted( 324 | {this.sessionId, this.siteId, this.customData, this.lang}); 325 | 326 | DialogueSessionStarted.fromJson(Map json) { 327 | sessionId = json['sessionId']; 328 | siteId = json['siteId']; 329 | customData = json['customData']; 330 | lang = json['lang']; 331 | } 332 | 333 | Map toJson() { 334 | final Map data = new Map(); 335 | data['sessionId'] = this.sessionId; 336 | data['siteId'] = this.siteId; 337 | data['customData'] = this.customData; 338 | data['lang'] = this.lang; 339 | return data; 340 | } 341 | } 342 | 343 | class DialogueSessionEnded { 344 | Termination termination; 345 | String sessionId; 346 | String siteId; 347 | String customData; 348 | 349 | DialogueSessionEnded( 350 | {this.termination, this.sessionId, this.siteId, this.customData}); 351 | 352 | DialogueSessionEnded.fromJson(Map json) { 353 | termination = json['termination'] != null 354 | ? new Termination.fromJson(json['termination']) 355 | : null; 356 | sessionId = json['sessionId']; 357 | siteId = json['siteId']; 358 | customData = json['customData']; 359 | } 360 | 361 | Map toJson() { 362 | final Map data = new Map(); 363 | if (this.termination != null) { 364 | data['termination'] = this.termination.toJson(); 365 | } 366 | data['sessionId'] = this.sessionId; 367 | data['siteId'] = this.siteId; 368 | data['customData'] = this.customData; 369 | return data; 370 | } 371 | } 372 | 373 | class Termination { 374 | String reason; 375 | 376 | Termination({this.reason}); 377 | 378 | Termination.fromJson(Map json) { 379 | reason = json['reason']; 380 | } 381 | 382 | Map toJson() { 383 | final Map data = new Map(); 384 | data['reason'] = this.reason; 385 | return data; 386 | } 387 | } 388 | 389 | class NluIntentNotRecognized { 390 | String input; 391 | String siteId; 392 | String id; 393 | String customData; 394 | String sessionId; 395 | 396 | NluIntentNotRecognized( 397 | {this.input, this.siteId, this.id, this.customData, this.sessionId}); 398 | 399 | NluIntentNotRecognized.fromJson(Map json) { 400 | input = json['input']; 401 | siteId = json['siteId']; 402 | id = json['id']; 403 | customData = json['customData']; 404 | sessionId = json['sessionId']; 405 | } 406 | 407 | Map toJson() { 408 | final Map data = new Map(); 409 | data['input'] = this.input; 410 | data['siteId'] = this.siteId; 411 | data['id'] = this.id; 412 | data['customData'] = this.customData; 413 | data['sessionId'] = this.sessionId; 414 | return data; 415 | } 416 | } 417 | 418 | class HotwordDetected { 419 | String modelId; 420 | String modelVersion; 421 | String modelType; 422 | double currentSensitivity; 423 | String siteId; 424 | String sessionId; 425 | bool sendAudioCaptured; 426 | String lang; 427 | 428 | HotwordDetected( 429 | {this.modelId, 430 | this.modelVersion, 431 | this.modelType, 432 | this.currentSensitivity, 433 | this.siteId, 434 | this.sessionId, 435 | this.sendAudioCaptured, 436 | this.lang}); 437 | 438 | HotwordDetected.fromJson(Map json) { 439 | modelId = json['modelId']; 440 | modelVersion = json['modelVersion']; 441 | modelType = json['modelType']; 442 | currentSensitivity = json['currentSensitivity']; 443 | siteId = json['siteId']; 444 | sessionId = json['sessionId']; 445 | sendAudioCaptured = json['sendAudioCaptured']; 446 | lang = json['lang']; 447 | } 448 | 449 | Map toJson() { 450 | final Map data = new Map(); 451 | data['modelId'] = this.modelId; 452 | data['modelVersion'] = this.modelVersion; 453 | data['modelType'] = this.modelType; 454 | data['currentSensitivity'] = this.currentSensitivity; 455 | data['siteId'] = this.siteId; 456 | data['sessionId'] = this.sessionId; 457 | data['sendAudioCaptured'] = this.sendAudioCaptured; 458 | data['lang'] = this.lang; 459 | return data; 460 | } 461 | } 462 | 463 | class HotwordToggle { 464 | String siteId; 465 | String reason; 466 | 467 | HotwordToggle({this.siteId, this.reason}); 468 | 469 | HotwordToggle.fromJson(Map json) { 470 | siteId = json['siteId']; 471 | reason = json['reason']; 472 | } 473 | 474 | Map toJson() { 475 | final Map data = new Map(); 476 | data['siteId'] = this.siteId; 477 | data['reason'] = this.reason; 478 | return data; 479 | } 480 | } 481 | 482 | class AudioSetVolume { 483 | double volume; 484 | String siteId; 485 | 486 | AudioSetVolume({this.volume, this.siteId}); 487 | 488 | AudioSetVolume.fromJson(Map json) { 489 | volume = json['volume']; 490 | siteId = json['siteId']; 491 | } 492 | 493 | Map toJson() { 494 | final Map data = new Map(); 495 | data['volume'] = this.volume; 496 | data['siteId'] = this.siteId; 497 | return data; 498 | } 499 | } 500 | -------------------------------------------------------------------------------- /lib/rhasspy_dart/rhasspy_api.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | import 'dart:typed_data'; 3 | import 'dart:convert'; 4 | import 'package:dio/adapter.dart'; 5 | import 'package:dio/dio.dart'; 6 | import 'package:rhasspy_mobile_app/rhasspy_dart/parse_messages.dart'; 7 | 8 | enum ProfileLayers { 9 | all, 10 | defaults, 11 | profile, 12 | } 13 | 14 | class MqttSettings { 15 | bool enabled = false; 16 | String host = ""; 17 | String password = ""; 18 | List siteIds = []; 19 | String username = ""; 20 | int port; 21 | 22 | MqttSettings( 23 | {this.enabled, this.host, this.password, this.siteIds, this.username}); 24 | MqttSettings.empty() : enabled = false; 25 | MqttSettings.fromJson(Map json) { 26 | enabled = json.containsKey("enabled") 27 | ? json['enabled'] == "true" || json['enabled'] == true 28 | ? true 29 | : false 30 | : false; 31 | host = json['host']; 32 | password = json['password']; 33 | if (json.containsKey("site_id")) { 34 | siteIds = (json["site_id"] as String).split(","); 35 | } else { 36 | siteIds = []; 37 | } 38 | 39 | siteIds = json["site_id"] != null && json["site_id"] != "" 40 | ? (json['site_id'] as String).split(",") 41 | : []; 42 | username = json['username']; 43 | port = json.containsKey("port") ? int.parse(json["port"]) : 1883; 44 | } 45 | 46 | Map toJson() { 47 | final Map data = new Map(); 48 | data['enabled'] = this.enabled; 49 | data['host'] = this.host; 50 | data['password'] = this.password; 51 | data['site_id'] = 52 | this.siteIds.length != 1 ? this.siteIds.join(",") : this.siteIds[0]; 53 | data['username'] = this.username; 54 | return data; 55 | } 56 | } 57 | 58 | class RhasspyProfile { 59 | Map _profile; 60 | MqttSettings mqttSettings; 61 | List get siteIds => mqttSettings.siteIds; 62 | bool get isMqttEnable => mqttSettings.enabled; 63 | set siteIds(siteId) { 64 | if (siteId is String) { 65 | mqttSettings.siteIds.add(siteId); 66 | } else { 67 | mqttSettings.siteIds.addAll(siteId); 68 | } 69 | } 70 | 71 | bool get isDialogueRhasspy { 72 | if (_profile.containsKey("dialogue") && 73 | _profile["dialogue"]["system"] == "rhasspy") { 74 | return true; 75 | } else { 76 | return false; 77 | } 78 | } 79 | 80 | void setDialogueSystem(String system) { 81 | if (!_profile.containsKey("dialogue")) { 82 | _profile["dialogue"] = Map(); 83 | } 84 | _profile["dialogue"]["system"] = system; 85 | } 86 | 87 | bool containsSiteId(String siteId) { 88 | for (String item in mqttSettings.siteIds) { 89 | if (siteId == item) return true; 90 | } 91 | return false; 92 | } 93 | 94 | bool isNewInstallation() { 95 | if (_profile.containsKey("language")) { 96 | return true; 97 | } else { 98 | return false; 99 | } 100 | } 101 | 102 | bool compareMqttSettings( 103 | String host, String username, String password, int port) { 104 | if (mqttSettings.host == host && 105 | mqttSettings.username == username && 106 | mqttSettings.password == password && 107 | mqttSettings.port == port) { 108 | return true; 109 | } else { 110 | return false; 111 | } 112 | } 113 | 114 | void addSiteId(String siteId) { 115 | if (containsSiteId(siteId)) return; 116 | mqttSettings.siteIds.add(siteId); 117 | } 118 | 119 | RhasspyProfile.fromJson(Map json) { 120 | _profile = json; 121 | mqttSettings = _profile.containsKey("mqtt") 122 | ? MqttSettings.fromJson(_profile["mqtt"]) 123 | : MqttSettings.empty(); 124 | } 125 | Map toJson() { 126 | _profile["mqtt"] = mqttSettings.toJson(); 127 | return _profile; 128 | } 129 | } 130 | 131 | class RhasspyApi { 132 | String ip; 133 | int port; 134 | bool ssl; 135 | String baseUrl; 136 | Dio dio; 137 | SecurityContext securityContext; 138 | 139 | static const Map profileString = { 140 | ProfileLayers.all: "all", 141 | ProfileLayers.defaults: "defaults", 142 | ProfileLayers.profile: "profile" 143 | }; 144 | 145 | RhasspyApi(this.ip, this.port, this.ssl, {this.securityContext}) { 146 | if (!ssl) { 147 | baseUrl = "http://" + ip + ":" + port.toString(); 148 | dio = Dio(BaseOptions(baseUrl: baseUrl)); 149 | } else { 150 | baseUrl = "https://" + ip + ":" + port.toString(); 151 | dio = Dio(BaseOptions(baseUrl: baseUrl)); 152 | (dio.httpClientAdapter as DefaultHttpClientAdapter).onHttpClientCreate = 153 | (client) { 154 | return HttpClient(context: securityContext); 155 | }; 156 | } 157 | } 158 | 159 | /// check if it is possible to establish a connection with rhasspy. 160 | /// Its return codes are 0 connection successfully, 161 | /// 1 connection failed, 2 bad certificate. 162 | Future checkConnection() async { 163 | // Recreate the object to change the timeout parameters 164 | Dio dio = Dio( 165 | BaseOptions(baseUrl: baseUrl, connectTimeout: 1000, receiveTimeout: 1000), 166 | ); 167 | (dio.httpClientAdapter as DefaultHttpClientAdapter).onHttpClientCreate = 168 | (client) { 169 | return HttpClient(context: securityContext); 170 | }; 171 | try { 172 | var response = await dio.get("/api/intents"); 173 | if (response.statusCode == 200) return 0; 174 | } on DioError catch (e) { 175 | if (e.error is HandshakeException) { 176 | return 2; 177 | } 178 | return 1; 179 | } 180 | return 0; 181 | } 182 | 183 | Future getIntent() async { 184 | Response response = await dio.get("/api/intents"); 185 | return response.data.toString(); 186 | } 187 | 188 | Future> getProfile(ProfileLayers layers) async { 189 | Response response = await dio.get("/api/profile", 190 | queryParameters: {"layers": profileString[layers]}, 191 | options: Options(responseType: ResponseType.json)); 192 | return response.data as Map; 193 | } 194 | 195 | Future setProfile(ProfileLayers layers, RhasspyProfile profile) async { 196 | Response response = await dio.post("/api/profile", 197 | queryParameters: {"layers": profileString[layers]}, 198 | data: profile.toJson(), 199 | options: Options(contentType: "application/json")); 200 | return response.statusCode == 200; 201 | } 202 | 203 | Future restart() async { 204 | Response response = await dio.post( 205 | "/api/restart", 206 | ); 207 | return response.statusCode == 200; 208 | } 209 | 210 | Future speechToIntent(File file) async { 211 | Response response = 212 | await dio.post("/api/speech-to-intent", data: file.openRead()); 213 | return response.data.toString(); 214 | } 215 | 216 | Future speechToText(File file) async { 217 | Response response = 218 | await dio.post("/api/speech-to-text", data: file.openRead()); 219 | return response.data; 220 | } 221 | 222 | Future textToIntent(String text) async { 223 | Response response = await dio.post("/api/text-to-intent", 224 | data: text, 225 | queryParameters: {"outputFormat": "hermes", "nohass": false}, 226 | options: Options(responseType: ResponseType.json)); 227 | return NluIntentParsed.fromJson(response.data["value"]); 228 | } 229 | 230 | Future textToSpeech(String text) async { 231 | Response response = await dio.post("/api/text-to-speech", 232 | data: text, options: Options(responseType: ResponseType.bytes)); 233 | return response.data; 234 | } 235 | } 236 | -------------------------------------------------------------------------------- /lib/rhasspy_dart/rhasspy_mqtt_isolate.dart: -------------------------------------------------------------------------------- 1 | import 'dart:typed_data'; 2 | 3 | import 'dart:async'; 4 | import 'dart:isolate'; 5 | 6 | import 'package:rhasspy_mobile_app/main.dart'; 7 | import 'package:rhasspy_mobile_app/wake_word/wake_word_base.dart'; 8 | 9 | import 'parse_messages.dart'; 10 | import 'rhasspy_mqtt_api.dart'; 11 | 12 | class RhasspyMqttIsolate { 13 | int port; 14 | String host; 15 | bool ssl; 16 | String username; 17 | String password; 18 | String siteId; 19 | bool autoRestart = true; 20 | Stream audioStream; 21 | Completer _isConnectedCompleter = Completer(); 22 | Future get isConnected async { 23 | if (_sendPort == null) { 24 | print("Starting to wait"); 25 | print(_isolateReady.isCompleted); 26 | await isReady; 27 | print("finished to wait"); 28 | } 29 | 30 | _sendPort.send("isConnected"); 31 | return _isConnectedCompleter.future; 32 | } 33 | 34 | int lastConnectionCode; 35 | WakeWordBase _wakeWordBase; 36 | 37 | bool isSessionManaged = false; 38 | String pemFilePath; 39 | SendPort _sendPort; 40 | ReceivePort _receivePort; 41 | 42 | /// the port to send message to isolate 43 | SendPort get sendPort => _sendPort; 44 | Isolate _isolate; 45 | Completer _connectCompleter = Completer(); 46 | Completer _isolateReady = Completer(); 47 | int timeOutIntent; 48 | void Function(NluIntentParsed) onReceivedIntent; 49 | void Function(AsrTextCaptured) onReceivedText; 50 | void Function(NluIntentNotRecognized) onIntentNotRecognized; 51 | Future Function(List) onReceivedAudio; 52 | void Function(DialogueEndSession) onReceivedEndSession; 53 | void Function(DialogueContinueSession) onReceivedContinueSession; 54 | void Function(HotwordDetected) onHotwordDetected; 55 | void Function(NluIntentParsed) onTimeoutIntentHandle; 56 | void Function() stopRecording; 57 | Future Function() startRecording; 58 | void Function(DialogueStartSession) onStartSession; 59 | void Function() onConnected; 60 | void Function() onDisconnected; 61 | void Function(double volume) onSetVolume; 62 | 63 | @override 64 | Future get connected async { 65 | if ((await _connectCompleter.future) == 0) { 66 | return true; 67 | } else { 68 | return false; 69 | } 70 | } 71 | 72 | Future get isReady => _isolateReady.future; 73 | 74 | RhasspyMqttIsolate( 75 | this.host, this.port, this.ssl, this.username, this.password, this.siteId, 76 | {this.timeOutIntent = 4, 77 | this.onReceivedIntent, 78 | this.onReceivedText, 79 | this.onReceivedAudio, 80 | this.onReceivedEndSession, 81 | this.onReceivedContinueSession, 82 | this.onTimeoutIntentHandle, 83 | this.onIntentNotRecognized, 84 | this.onHotwordDetected, 85 | this.onConnected, 86 | this.onDisconnected, 87 | this.onStartSession, 88 | this.startRecording, 89 | this.audioStream, 90 | this.stopRecording, 91 | this.onSetVolume, 92 | this.pemFilePath}) { 93 | _init(); 94 | } 95 | 96 | void _init() { 97 | _initIsolate(); 98 | _initializeMqtt(); 99 | audioStream?.listen((event) { 100 | _sendPort.send(event); 101 | }); 102 | } 103 | 104 | void dispose() { 105 | _isolate.kill(); 106 | } 107 | 108 | void subscribeCallback({ 109 | void Function(NluIntentParsed) onReceivedIntent, 110 | void Function(AsrTextCaptured) onReceivedText, 111 | Future Function(List) onReceivedAudio, 112 | void Function(DialogueEndSession) onReceivedEndSession, 113 | void Function(DialogueContinueSession) onReceivedContinueSession, 114 | void Function(NluIntentParsed) onTimeoutIntentHandle, 115 | void Function(NluIntentNotRecognized) onIntentNotRecognized, 116 | void Function(HotwordDetected) onHotwordDetected, 117 | void Function() onConnected, 118 | void Function() onDisconnected, 119 | void Function(DialogueStartSession) onStartSession, 120 | Future Function() startRecording, 121 | void Function(double volume) onSetVolume, 122 | void Function() stopRecording, 123 | Stream audioStream, 124 | }) { 125 | this.onReceivedIntent = onReceivedIntent; 126 | this.onReceivedText = onReceivedText; 127 | this.onReceivedAudio = onReceivedAudio; 128 | this.onReceivedEndSession = onReceivedEndSession; 129 | this.onReceivedContinueSession = onReceivedContinueSession; 130 | this.onTimeoutIntentHandle = onTimeoutIntentHandle; 131 | this.onIntentNotRecognized = onIntentNotRecognized; 132 | this.onHotwordDetected = onHotwordDetected; 133 | this.onStartSession = onStartSession; 134 | this.onConnected = onConnected; 135 | this.onDisconnected = onDisconnected; 136 | this.startRecording = startRecording; 137 | this.stopRecording = stopRecording; 138 | this.onSetVolume = onSetVolume; 139 | this.audioStream = audioStream; 140 | audioStream?.listen((event) { 141 | _sendPort.send(event); 142 | }); 143 | } 144 | 145 | Future connect() async { 146 | if (_sendPort == null) { 147 | await _isolateReady.future; 148 | } 149 | _sendPort.send("connect"); 150 | return _connectCompleter.future; 151 | } 152 | 153 | Future _initIsolate() async { 154 | _receivePort = ReceivePort(); 155 | final errorPort = ReceivePort(); 156 | errorPort.listen((error) { 157 | if (error is List) { 158 | if (error[0].contains("SocketException:")) { 159 | connect(); 160 | } 161 | } 162 | }); 163 | 164 | _receivePort.listen(_handleMessage); 165 | _isolate = await Isolate.spawn(_isolateEntry, _receivePort.sendPort, 166 | onError: errorPort.sendPort, debugName: "Mqtt", errorsAreFatal: false); 167 | _isolate.addOnExitListener(_receivePort.sendPort, response: "exit"); 168 | } 169 | 170 | Future _initializeMqtt() async { 171 | RhasspyMqttArguments rhasspyMqttArguments = RhasspyMqttArguments( 172 | this.host, 173 | this.port, 174 | this.ssl, 175 | this.username, 176 | this.password, 177 | this.siteId, 178 | this.pemFilePath, 179 | timeOutIntent: this.timeOutIntent, 180 | enableStream: this.audioStream == null ? false : true); 181 | await _isolateReady.future; 182 | _sendPort.send(rhasspyMqttArguments); 183 | } 184 | 185 | void update(String host, int port, bool ssl, String username, String password, 186 | String siteId, String pemFilePath) { 187 | RhasspyMqttArguments rhasspyMqttArguments = RhasspyMqttArguments( 188 | host, 189 | port, 190 | ssl, 191 | username, 192 | password, 193 | siteId, 194 | pemFilePath, 195 | ); 196 | _sendPort.send(rhasspyMqttArguments); 197 | } 198 | 199 | void _handleMessage(dynamic message) async { 200 | if (message is SendPort) { 201 | _sendPort = message; 202 | _isolateReady.complete(); 203 | _isolateReady = Completer(); 204 | return; 205 | } 206 | if (message is String) { 207 | switch (message) { 208 | case "stopRecording": 209 | stopRecording(); 210 | break; 211 | case "startRecording": 212 | startRecording().then((value) { 213 | _sendPort.send({"startRecording": value}); 214 | }); 215 | break; 216 | case "onConnected": 217 | _connectCompleter.complete(0); 218 | _connectCompleter = Completer(); 219 | if (onConnected != null) onConnected(); 220 | break; 221 | case "onDisconnected": 222 | if (onDisconnected != null) onDisconnected(); 223 | break; 224 | case "exit": 225 | if (autoRestart) { 226 | _init(); 227 | } 228 | break; 229 | default: 230 | } 231 | } 232 | if (message is Map) { 233 | switch (message.keys.first) { 234 | case "connectCode": 235 | _connectCompleter.complete(message["connectCode"]); 236 | _connectCompleter = Completer(); 237 | lastConnectionCode = message["connectCode"]; 238 | break; 239 | case "onReceivedAudio": 240 | onReceivedAudio(message["onReceivedAudio"]) 241 | .then((value) => _sendPort.send({"onReceivedAudio": value})); 242 | break; 243 | case "onReceivedText": 244 | onReceivedText(message["onReceivedText"]); 245 | break; 246 | case "onReceivedIntent": 247 | onReceivedIntent(message["onReceivedIntent"]); 248 | break; 249 | case "onReceivedEndSession": 250 | onReceivedEndSession(message["onReceivedEndSession"]); 251 | break; 252 | case "onReceivedContinueSession": 253 | onReceivedContinueSession(message["onReceivedContinueSession"]); 254 | break; 255 | case "onTimeoutIntentHandle": 256 | onTimeoutIntentHandle(message["onTimeoutIntentHandle"]); 257 | break; 258 | case "onStartSession": 259 | if (onStartSession != null) onStartSession(message["onStartSession"]); 260 | break; 261 | case "isSessionManaged": 262 | isSessionManaged = message["isSessionManaged"]; 263 | break; 264 | case "onIntentNotRecognized": 265 | onIntentNotRecognized(message["onIntentNotRecognized"]); 266 | break; 267 | case "onHotwordDetected": 268 | onHotwordDetected(message["onHotwordDetected"]); 269 | break; 270 | case "IsConnected": 271 | _isConnectedCompleter.complete(message["IsConnected"]); 272 | _isConnectedCompleter = Completer(); 273 | break; 274 | case "onSetVolume": 275 | onSetVolume(message["onSetVolume"]); 276 | break; 277 | case "print": 278 | String messageToPrint = message["print"]; 279 | if (messageToPrint.startsWith("[D] ")) { 280 | log.d(messageToPrint.replaceFirst("[D] ", ""), "MQTT"); 281 | } else if (messageToPrint.startsWith("[I] ")) { 282 | log.i(messageToPrint.replaceFirst("[I] ", ""), "MQTT"); 283 | } else if (messageToPrint.startsWith("[E] ")) { 284 | log.e(messageToPrint.replaceFirst("[E] ", ""), "MQTT"); 285 | } else if (messageToPrint.startsWith("[W] ")) { 286 | log.w(messageToPrint.replaceFirst("[W] ", ""), "MQTT"); 287 | } 288 | break; 289 | case "WakeWord": 290 | switch (message["WakeWord"]) { 291 | case "availableWakeWordDetector": 292 | sendPort.send({ 293 | "availableWakeWordDetector": 294 | await _wakeWordBase.availableWakeWordDetector 295 | }); 296 | break; 297 | case "isRunning": 298 | sendPort.send({"isRunning": await _wakeWordBase.isRunning}); 299 | break; 300 | case "pause": 301 | sendPort.send({"pause": await _wakeWordBase.pause()}); 302 | break; 303 | case "resume": 304 | sendPort.send({"resume": await _wakeWordBase.resume()}); 305 | break; 306 | default: 307 | throw UnimplementedError( 308 | "Undefined behavior for message: $message"); 309 | } 310 | break; 311 | default: 312 | throw UnimplementedError("Undefined behavior for message: $message"); 313 | } 314 | } 315 | } 316 | 317 | static void _isolateEntry(dynamic message) async { 318 | SendPort sendPort; 319 | runZoned(() { 320 | RhasspyMqttApi rhasspyMqtt; 321 | final receivePort = ReceivePort(); 322 | Completer _receivedAudio = Completer(); 323 | Completer _startRecordingCompleter = Completer(); 324 | StreamController audioStream = StreamController(); 325 | WakeWordBaseIsolate _wakeWordIsolate; 326 | receivePort.listen( 327 | (dynamic message) async { 328 | if (message is RhasspyMqttArguments) { 329 | if (rhasspyMqtt != null) { 330 | rhasspyMqtt.dispose(); 331 | audioStream.close(); 332 | audioStream = StreamController(); 333 | } 334 | rhasspyMqtt = RhasspyMqttApi(message.host, message.port, 335 | message.ssl, message.username, message.password, message.siteId, 336 | timeOutIntent: message.timeOutIntent, 337 | pemFilePath: message.pemFilePath, 338 | audioStream: audioStream.stream, 339 | onReceivedAudio: (value) async { 340 | print("onReceivedAudio isolate"); 341 | sendPort.send({"onReceivedAudio": value}); 342 | return _receivedAudio.future; 343 | }, onReceivedText: (textCapture) { 344 | sendPort.send({"onReceivedText": textCapture}); 345 | }, onReceivedIntent: (intentParsed) { 346 | sendPort.send({"onReceivedIntent": intentParsed}); 347 | }, onReceivedEndSession: (endSession) { 348 | sendPort.send({"onReceivedEndSession": endSession}); 349 | sendPort.send({"isSessionManaged": false}); 350 | }, onReceivedContinueSession: (continueSession) { 351 | sendPort.send({"onReceivedContinueSession": continueSession}); 352 | }, onTimeoutIntentHandle: (intentParsed) { 353 | sendPort.send({"onTimeoutIntentHandle": intentParsed}); 354 | }, onConnected: () { 355 | sendPort.send("onConnected"); 356 | }, onDisconnected: () { 357 | sendPort.send("onDisconnected"); 358 | }, onHotwordDetected: (hotwordDetected) { 359 | sendPort.send({"onHotwordDetected": hotwordDetected}); 360 | }, onStartSession: (startSession) { 361 | sendPort.send({"onStartSession": startSession}); 362 | //TODO get isSessionStarted directly 363 | sendPort.send({"isSessionManaged": rhasspyMqtt.isSessionManaged}); 364 | }, stopRecording: () { 365 | sendPort.send("stopRecording"); 366 | }, startRecording: () { 367 | sendPort.send("startRecording"); 368 | return _startRecordingCompleter.future; 369 | }, onIntentNotRecognized: (intent) { 370 | sendPort.send({"onIntentNotRecognized": intent}); 371 | }, onSetVolume: (volume) { 372 | sendPort.send({"onSetVolume": volume}); 373 | }); 374 | } else if (message is String) { 375 | switch (message) { 376 | case "connect": 377 | int result = await rhasspyMqtt.connect(); 378 | sendPort.send({"connectCode": result}); 379 | break; 380 | case "stopListening": 381 | rhasspyMqtt.stopListening(); 382 | break; 383 | case "cleanSession": 384 | rhasspyMqtt.cleanSession(); 385 | break; 386 | case "enableWakeWord": 387 | _wakeWordIsolate = WakeWordBaseIsolate(sendPort: sendPort); 388 | rhasspyMqtt.enableWakeWord(_wakeWordIsolate); 389 | break; 390 | case "isConnected": 391 | sendPort.send({"IsConnected": rhasspyMqtt.isConnected}); 392 | break; 393 | default: 394 | throw UnimplementedError( 395 | "Undefined behavior for message: $message"); 396 | } 397 | } else if (message is Map) { 398 | switch (message.keys.first) { 399 | case "onReceivedAudio": 400 | _receivedAudio.complete(message["onReceivedAudio"]); 401 | _receivedAudio = Completer(); 402 | break; 403 | case "speechTotext": 404 | rhasspyMqtt.speechTotext(message["speechTotext"]["dataAudio"], 405 | cleanSession: message["speechTotext"]["cleanSession"]); 406 | break; 407 | case "textToIntent": 408 | rhasspyMqtt.textToIntent(message["textToIntent"]["text"], 409 | handle: message["textToIntent"]["handle"]); 410 | break; 411 | case "textToSpeech": 412 | rhasspyMqtt.textToSpeech(message["textToSpeech"]["text"], 413 | generateSessionId: message["textToSpeech"] 414 | ["generateSessionId"]); 415 | break; 416 | case "enableWakeWord": 417 | rhasspyMqtt.enableWakeWord(message["enableWakeWord"]); 418 | break; 419 | case "startRecording": 420 | _startRecordingCompleter.complete(message["startRecording"]); 421 | _startRecordingCompleter = Completer(); 422 | break; 423 | case "isRunning": 424 | _wakeWordIsolate.isRunningCompleter 425 | .complete(message["isRunning"]); 426 | _wakeWordIsolate.isRunningCompleter = Completer(); 427 | break; 428 | case "wake": 429 | rhasspyMqtt.wake( 430 | message["wake"]["hotWord"], message["wake"]["wakeWordId"]); 431 | break; 432 | case "availableWakeWordDetector": 433 | _wakeWordIsolate.availableWakeWordDetectorCompleter 434 | .complete(message["availableWakeWordDetector"]); 435 | _wakeWordIsolate.availableWakeWordDetectorCompleter = 436 | Completer(); 437 | break; 438 | default: 439 | throw UnimplementedError( 440 | "Undefined behavior for message: $message"); 441 | } 442 | } else if (message is Uint8List) { 443 | audioStream.add(message); 444 | } 445 | }, 446 | ); 447 | 448 | if (message is SendPort) { 449 | sendPort = message; 450 | sendPort.send(receivePort.sendPort); 451 | return; 452 | } 453 | }, zoneSpecification: 454 | ZoneSpecification(print: (self, parent, zone, message) { 455 | sendPort.send({"print": message}); 456 | })); 457 | } 458 | 459 | @override 460 | void speechTotext(Uint8List dataAudio, {bool cleanSession = true}) { 461 | _sendPort.send({ 462 | "speechTotext": {"dataAudio": dataAudio, "cleanSession": cleanSession} 463 | }); 464 | } 465 | 466 | void enableWakeWord(WakeWordBase wakeWord) { 467 | _wakeWordBase = wakeWord; 468 | _sendPort.send("enableWakeWord"); 469 | } 470 | 471 | @override 472 | void stopListening() { 473 | _sendPort.send("stopListening"); 474 | } 475 | 476 | @override 477 | void textToIntent(String text, {bool handle = true}) { 478 | _sendPort.send({ 479 | "textToIntent": {"text": text, "handle": handle} 480 | }); 481 | } 482 | 483 | @override 484 | void textToSpeech(String text, {bool generateSessionId = false}) { 485 | _sendPort.send({ 486 | "textToSpeech": {"text": text, "generateSessionId": generateSessionId} 487 | }); 488 | } 489 | 490 | void cleanSession() { 491 | _sendPort.send("cleanSession"); 492 | } 493 | 494 | void wake(HotwordDetected hotWord, String wakeWordId) { 495 | _sendPort.send({ 496 | "wake": {"hotWord": hotWord, "wakeWordId": wakeWordId} 497 | }); 498 | } 499 | } 500 | 501 | class RhasspyMqttArguments { 502 | int port; 503 | String host; 504 | bool ssl; 505 | String username; 506 | String password; 507 | String siteId; 508 | String pemFilePath; 509 | int timeOutIntent; 510 | bool enableStream; 511 | RhasspyMqttArguments( 512 | this.host, 513 | this.port, 514 | this.ssl, 515 | this.username, 516 | this.password, 517 | this.siteId, 518 | this.pemFilePath, { 519 | this.timeOutIntent = 4, 520 | this.enableStream = false, 521 | }); 522 | } 523 | 524 | class WakeWordBaseIsolate implements WakeWordBase { 525 | SendPort sendPort; 526 | WakeWordBaseIsolate({this.sendPort}); 527 | Completer isRunningCompleter = Completer(); 528 | Completer resumeCompleter = Completer(); 529 | Completer pauseCompleter = Completer(); 530 | Completer isListeningCompleter = Completer(); 531 | Completer> availableWakeWordDetectorCompleter = Completer(); 532 | 533 | @override 534 | String name; 535 | 536 | @override 537 | Future> get availableWakeWordDetector { 538 | sendPort.send({"WakeWord": "availableWakeWordDetector"}); 539 | return availableWakeWordDetectorCompleter.future; 540 | } 541 | 542 | @override 543 | Future get isAvailable => throw UnimplementedError(); 544 | 545 | @override 546 | Future get isRunning { 547 | sendPort.send({"WakeWord": "isRunning"}); 548 | return isRunningCompleter.future; 549 | } 550 | 551 | @override 552 | Future pause() { 553 | sendPort.send({"WakeWord": "pause"}); 554 | return pauseCompleter.future; 555 | } 556 | 557 | @override 558 | Future resume() { 559 | sendPort.send({"WakeWord": "resume"}); 560 | return resumeCompleter.future; 561 | } 562 | 563 | @override 564 | Future startListening() { 565 | throw UnimplementedError(); 566 | } 567 | 568 | @override 569 | Future stopListening() { 570 | sendPort.send({"WakeWord": "stopListening"}); 571 | } 572 | 573 | @override 574 | Future get isListening { 575 | sendPort.send({"WakeWord": "stopListening"}); 576 | return isListeningCompleter.future; 577 | } 578 | } 579 | -------------------------------------------------------------------------------- /lib/rhasspy_dart/utility/rhasspy_mqtt_logger.dart: -------------------------------------------------------------------------------- 1 | enum Level { debug, info, warning, error } 2 | 3 | class Logger { 4 | static bool loggingEnable = true; 5 | static const prefix = { 6 | Level.debug: "[D]", 7 | Level.info: "[I]", 8 | Level.warning: "[W]", 9 | Level.error: "[E]" 10 | }; 11 | void log(String message, Level level) { 12 | if (loggingEnable) print("${prefix[level]} $message"); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /lib/utils/audio_recorder_isolate.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:io'; 3 | import 'dart:isolate'; 4 | import 'dart:typed_data'; 5 | import 'package:flutter_audio_recorder/flutter_audio_recorder.dart'; 6 | import 'package:path_provider/path_provider.dart'; 7 | import 'package:rhasspy_mobile_app/utils/utils.dart'; 8 | 9 | class AudioRecorderIsolate { 10 | SendPort _sendPort; 11 | 12 | Isolate _isolate; 13 | SendPort otherIsolate; 14 | final _isolateReady = Completer(); 15 | FlutterAudioRecorder _recorder; 16 | Future get isRecording async { 17 | if ((await _recorder?.current())?.status == RecordingStatus.Recording) { 18 | return true; 19 | } else { 20 | return false; 21 | } 22 | } 23 | 24 | AudioRecorderIsolate({this.otherIsolate}) { 25 | init(); 26 | setOtherIsolate(otherIsolate); 27 | } 28 | 29 | Future get isReady => _isolateReady.future; 30 | 31 | void dispose() { 32 | _isolate.kill(); 33 | } 34 | 35 | Future init() async { 36 | final receivePort = ReceivePort(); 37 | final errorPort = ReceivePort(); 38 | errorPort.listen(print); 39 | receivePort.listen(_handleMessage); 40 | _isolate = await Isolate.spawn(_isolateEntry, receivePort.sendPort, 41 | onError: errorPort.sendPort, 42 | debugName: "recorder", 43 | errorsAreFatal: false); 44 | } 45 | 46 | Future setOtherIsolate([SendPort otherIsolate]) async { 47 | await isReady; 48 | if (otherIsolate == null) otherIsolate = this.otherIsolate; 49 | _sendPort.send(otherIsolate); 50 | } 51 | 52 | void _handleMessage(dynamic message) { 53 | if (message is SendPort) { 54 | _sendPort = message; 55 | _isolateReady.complete(); 56 | return; 57 | } 58 | } 59 | 60 | Future startRecording() async { 61 | Directory appDocDirectory = await getApplicationDocumentsDirectory(); 62 | String pathFile = appDocDirectory.path + "/speech_to_text.wav"; 63 | if (File(pathFile).existsSync()) File(pathFile).deleteSync(); 64 | _recorder = FlutterAudioRecorder(pathFile, audioFormat: AudioFormat.WAV); 65 | await _recorder.initialized; 66 | await _recorder.start(); 67 | _sendPort.send({"StartRecording": pathFile}); 68 | } 69 | 70 | void stopRecording() { 71 | if (_recorder != null) { 72 | _recorder.stop(); 73 | _sendPort.send("StopRecording"); 74 | } 75 | } 76 | 77 | static Future _isolateEntry(dynamic message) async { 78 | SendPort sendPort; 79 | SendPort otherIsolate; 80 | final receivePort = ReceivePort(); 81 | Timer timerForAudio; 82 | receivePort.listen((dynamic message) async { 83 | print("message $message is a istance of ${message.runtimeType}"); 84 | if (message is Map) { 85 | if (message.containsKey("StartRecording")) { 86 | int previousLength = 0; 87 | // prepare the file that will contain the audio stream 88 | File audioFile = File(message["StartRecording"] + ".temp"); 89 | int chunkSize = 2048; 90 | int byteRate = (16000 * 16 * 1 ~/ 8); 91 | bool active = false; 92 | timerForAudio = 93 | Timer.periodic(Duration(milliseconds: 1), (Timer t) async { 94 | int fileLength; 95 | try { 96 | fileLength = audioFile.lengthSync(); 97 | } on FileSystemException { 98 | t.cancel(); 99 | } 100 | // if a chunk is available to send 101 | if ((fileLength - previousLength) >= chunkSize) { 102 | if (active) { 103 | return; 104 | } 105 | active = true; 106 | // start reading the last chunk 107 | Stream> dataStream = audioFile.openRead( 108 | previousLength, previousLength + chunkSize); 109 | List dataFile = []; 110 | dataStream.listen( 111 | (data) { 112 | dataFile += data; 113 | }, 114 | onDone: () async { 115 | if (dataFile.isNotEmpty) { 116 | print("previous length: $previousLength"); 117 | print("Length: $fileLength"); 118 | previousLength += chunkSize; 119 | // append the header to the beginning of the chunk 120 | Uint8List header = 121 | waveHeader(chunkSize, 16000, 1, byteRate); 122 | dataFile.insertAll(0, header); 123 | otherIsolate.send(Uint8List.fromList(dataFile)); 124 | // audioStreamcontroller.add(Uint8List.fromList(dataFile)); 125 | active = false; 126 | } 127 | }, 128 | ); 129 | } 130 | }); 131 | } 132 | } 133 | if (message is String) { 134 | switch (message) { 135 | case "StopRecording": 136 | timerForAudio.cancel(); 137 | break; 138 | default: 139 | } 140 | } 141 | if (message is SendPort) { 142 | otherIsolate = message; 143 | } 144 | }); 145 | if (message is SendPort) { 146 | if (sendPort == null) { 147 | sendPort = message; 148 | sendPort.send(receivePort.sendPort); 149 | return; 150 | } 151 | } 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /lib/utils/constants.dart: -------------------------------------------------------------------------------- 1 | const String appTag = "APP"; 2 | const String mqttTag = "MQTT"; 3 | const String rhasspyTag = "RHASSPY"; 4 | const String applicationVersion = "1.7.0"; 5 | -------------------------------------------------------------------------------- /lib/utils/logger/log_page.dart: -------------------------------------------------------------------------------- 1 | import 'logger.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter/services.dart'; 4 | import 'package:intl/intl.dart'; 5 | 6 | void openLogPage(BuildContext context, Logger logger) { 7 | Navigator.push( 8 | context, 9 | MaterialPageRoute( 10 | builder: (context) => LogPage( 11 | logger: logger, 12 | ), 13 | ), 14 | ); 15 | } 16 | 17 | class LogPage extends StatefulWidget { 18 | final Logger logger; 19 | final double textSize; 20 | LogPage({Key key, this.logger, this.textSize = 20}) : super(key: key); 21 | 22 | @override 23 | _LogPageState createState() => _LogPageState(); 24 | } 25 | 26 | class _LogPageState extends State { 27 | MemoryLogOutput memoryOutput; 28 | FileOutput fileOutput; 29 | DateFormat formatter = DateFormat.Hms(); 30 | 31 | @override 32 | Widget build(BuildContext context) { 33 | return Scaffold( 34 | appBar: AppBar( 35 | title: Text("Log"), 36 | actions: [ 37 | Tooltip( 38 | message: "Copy log to clipBoard", 39 | child: IconButton( 40 | icon: const Icon(Icons.copy), 41 | onPressed: () { 42 | String text = ""; 43 | if (fileOutput != null) { 44 | fileOutput.readLogs().forEach((line) { 45 | text += "$line\n"; 46 | }); 47 | } else { 48 | SimplePrinter printer = 49 | SimplePrinter(includeStackTrace: true); 50 | printer.init(); 51 | for (var log in memoryOutput.buffer) { 52 | printer.log(log).forEach((line) { 53 | text += line; 54 | }); 55 | } 56 | } 57 | Clipboard.setData(ClipboardData(text: text)); 58 | }, 59 | ), 60 | ), 61 | ], 62 | leading: IconButton( 63 | icon: const Icon(Icons.arrow_back), 64 | onPressed: () { 65 | Navigator.pop(context); 66 | }, 67 | ), 68 | ), 69 | body: ListView.builder( 70 | itemBuilder: (context, index) { 71 | LogMessage logMessage = memoryOutput.buffer.elementAt(index); 72 | return Card( 73 | elevation: 5, 74 | color: levelToColor(logMessage.logLevel), 75 | child: Column( 76 | crossAxisAlignment: CrossAxisAlignment.start, 77 | children: [ 78 | Padding( 79 | padding: const EdgeInsets.only(top: 5, left: 5, right: 5), 80 | child: Row( 81 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 82 | children: [ 83 | if (logMessage.title != null) 84 | Text( 85 | logMessage.title, 86 | style: TextStyle(fontSize: widget.textSize), 87 | ), 88 | if (logMessage.time != null) 89 | Text( 90 | formatter.format(logMessage.time), 91 | style: TextStyle(fontSize: widget.textSize), 92 | ), 93 | ], 94 | ), 95 | ), 96 | Padding( 97 | padding: 98 | const EdgeInsets.symmetric(horizontal: 5, vertical: 5), 99 | child: Text( 100 | logMessage.message, 101 | textAlign: TextAlign.left, 102 | style: TextStyle(fontSize: widget.textSize - 5), 103 | ), 104 | ), 105 | if (logMessage.stackTrace != null) 106 | //TODO fix set state when animation is in progress 107 | StackTraceWidget( 108 | logMessage.stackTrace, 109 | duration: const Duration(milliseconds: 400), 110 | ) 111 | else 112 | SizedBox( 113 | width: 10, 114 | height: 10, 115 | ) 116 | ], 117 | ), 118 | ); 119 | }, 120 | itemCount: memoryOutput.buffer.length, 121 | ), 122 | ); 123 | } 124 | 125 | @override 126 | void initState() { 127 | if (widget.logger.logOutput is MemoryLogOutput) { 128 | memoryOutput = widget.logger.logOutput; 129 | } else if (widget.logger.logOutput is MultiOutput) { 130 | for (var logOuput 131 | in (widget.logger.logOutput as MultiOutput).logOutputs) { 132 | if (logOuput is MemoryLogOutput) { 133 | memoryOutput = logOuput; 134 | break; 135 | } else if (logOuput is FileOutput) { 136 | fileOutput = logOuput; 137 | } 138 | } 139 | } 140 | memoryOutput.onUpdate = () { 141 | setState(() {}); 142 | }; 143 | super.initState(); 144 | } 145 | 146 | @override 147 | void dispose() { 148 | memoryOutput.onUpdate = () {}; 149 | super.dispose(); 150 | } 151 | 152 | Color levelToColor(Level level) { 153 | switch (level) { 154 | case Level.verbose: 155 | return Colors.lightBlue; 156 | break; 157 | case Level.debug: 158 | return Colors.cyan; 159 | break; 160 | case Level.info: 161 | return Colors.green; 162 | break; 163 | case Level.warning: 164 | return Colors.orange; 165 | break; 166 | case Level.error: 167 | return Colors.red; 168 | break; 169 | case Level.critical: 170 | return Colors.redAccent[700]; 171 | break; 172 | } 173 | } 174 | } 175 | 176 | class StackTraceWidget extends StatefulWidget { 177 | final Duration duration; 178 | final StackTrace stackTrace; 179 | final bool defaultOpen; 180 | StackTraceWidget(this.stackTrace, 181 | {Key key, 182 | this.duration = const Duration(milliseconds: 250), 183 | this.defaultOpen = false}) 184 | : super(key: key); 185 | 186 | @override 187 | _StackTraceWidgetState createState() => _StackTraceWidgetState(); 188 | } 189 | 190 | class _StackTraceWidgetState extends State 191 | with SingleTickerProviderStateMixin { 192 | bool isStackTraceVisible = false; 193 | @override 194 | Widget build(BuildContext context) { 195 | return Column( 196 | children: [ 197 | AnimatedSize( 198 | vsync: this, 199 | curve: Curves.easeIn, 200 | duration: widget.duration, 201 | child: Container( 202 | child: 203 | isStackTraceVisible ? Text(widget.stackTrace.toString()) : null, 204 | ), 205 | ), 206 | Center( 207 | child: AnimationRotationIconButton( 208 | duration: widget.duration, 209 | onPressed: () { 210 | setState(() { 211 | isStackTraceVisible = !isStackTraceVisible; 212 | }); 213 | }, 214 | ), 215 | ), 216 | ], 217 | ); 218 | } 219 | 220 | @override 221 | void initState() { 222 | isStackTraceVisible = widget.defaultOpen; 223 | super.initState(); 224 | } 225 | } 226 | 227 | class AnimationRotationIconButton extends StatefulWidget { 228 | final double startAngle; 229 | final double endAngle; 230 | final Duration duration; 231 | final Icon icon; 232 | final void Function() onPressed; 233 | AnimationRotationIconButton( 234 | {Key key, 235 | this.startAngle = 0, 236 | this.endAngle = 3.14, 237 | this.duration = const Duration(milliseconds: 250), 238 | this.icon = const Icon(Icons.arrow_drop_down), 239 | this.onPressed}) 240 | : super(key: key); 241 | 242 | @override 243 | _AnimationRotationIconButtonState createState() => 244 | _AnimationRotationIconButtonState(); 245 | } 246 | 247 | class _AnimationRotationIconButtonState 248 | extends State { 249 | double startAngle = 0; 250 | double endAngle = 0; 251 | bool isInStartCondition = true; 252 | @override 253 | Widget build(BuildContext context) { 254 | return Container( 255 | child: TweenAnimationBuilder( 256 | duration: widget.duration, 257 | curve: Curves.easeIn, 258 | tween: Tween(begin: startAngle, end: endAngle), 259 | child: widget.icon, 260 | builder: (BuildContext context, double size, Widget child) { 261 | return Transform.rotate( 262 | angle: size, 263 | child: IconButton( 264 | icon: child, 265 | onPressed: () { 266 | setState(() { 267 | if (isInStartCondition) { 268 | startAngle = widget.startAngle; 269 | endAngle = widget.endAngle; 270 | isInStartCondition = false; 271 | } else { 272 | endAngle = widget.startAngle; 273 | startAngle = widget.endAngle; 274 | isInStartCondition = true; 275 | } 276 | }); 277 | if (widget.onPressed != null) widget.onPressed(); 278 | }, 279 | iconSize: 32, 280 | ), 281 | ); 282 | }, 283 | ), 284 | ); 285 | } 286 | } 287 | -------------------------------------------------------------------------------- /lib/utils/logger/logger.dart: -------------------------------------------------------------------------------- 1 | // This file is based on work covered by the following copyright and permission notice: 2 | 3 | // MIT License 4 | 5 | // Copyright (c) 2019 Simon Leier 6 | 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | 14 | // The above copyright notice and this permission notice shall be included in all 15 | // copies or substantial portions of the Software. 16 | 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | // SOFTWARE. 24 | 25 | // If you want to get a logger which prints beautiful logs please visit https://github.com/leisim/logger. 26 | // This version includes the necessary changes for creating a nice flutter log page. 27 | 28 | import 'dart:async'; 29 | import 'dart:collection'; 30 | import 'dart:convert'; 31 | import 'dart:io'; 32 | 33 | /// [Level]s to control logging output. Logging can be enabled to include all 34 | /// levels above certain [Level]. 35 | enum Level { 36 | verbose, 37 | debug, 38 | info, 39 | warning, 40 | error, 41 | critical, 42 | } 43 | 44 | class LogMessage { 45 | String message; 46 | Level logLevel; 47 | StackTrace stackTrace; 48 | DateTime time; 49 | String title; 50 | LogMessage( 51 | {this.message, this.logLevel, this.stackTrace, this.time, this.title}); 52 | } 53 | 54 | abstract class LogOutput { 55 | void init() {} 56 | 57 | void output(LogMessage event); 58 | 59 | void dispose() {} 60 | } 61 | 62 | abstract class LogPrinter { 63 | void init() {} 64 | 65 | List log(LogMessage event); 66 | 67 | void destroy() {} 68 | } 69 | 70 | class MultiOutput implements LogOutput { 71 | List logOutputs; 72 | MultiOutput(this.logOutputs); 73 | @override 74 | void dispose() { 75 | logOutputs.forEach((logOutput) { 76 | logOutput.dispose(); 77 | }); 78 | } 79 | 80 | @override 81 | void init() { 82 | logOutputs.forEach((logOutput) { 83 | logOutput.init(); 84 | }); 85 | } 86 | 87 | @override 88 | void output(LogMessage event) { 89 | logOutputs.forEach((logOutput) { 90 | logOutput.output(event); 91 | }); 92 | } 93 | } 94 | 95 | class SimplePrinter implements LogPrinter { 96 | static final levelPrefixes = { 97 | Level.verbose: '[V]', 98 | Level.debug: '[D]', 99 | Level.info: '[I]', 100 | Level.warning: '[W]', 101 | Level.error: '[E]', 102 | Level.critical: '[C]', 103 | }; 104 | final bool includeStackTrace; 105 | const SimplePrinter({this.includeStackTrace = true}); 106 | 107 | @override 108 | List log(LogMessage event) { 109 | var messageStr = _stringifyMessage(event.message); 110 | var errorStr = event.title != null ? ' TAG: ${event.title}' : ''; 111 | var timeStr = 112 | event.time != null ? 'TIME: ${event.time.toIso8601String()}' : ''; 113 | var stackStrace = event.stackTrace != null && this.includeStackTrace 114 | ? event.stackTrace.toString() 115 | : ''; 116 | return [ 117 | '${levelPrefixes[event.logLevel]} $timeStr $messageStr$errorStr\n$stackStrace' 118 | ]; 119 | } 120 | 121 | String _stringifyMessage(dynamic message) { 122 | if (message is Map || message is Iterable) { 123 | var encoder = JsonEncoder.withIndent(null); 124 | return encoder.convert(message); 125 | } else { 126 | return message.toString(); 127 | } 128 | } 129 | 130 | @override 131 | void destroy() {} 132 | 133 | @override 134 | void init() {} 135 | } 136 | 137 | class FileOutput extends LogOutput { 138 | final File file; 139 | final bool overrideExisting; 140 | final Encoding encoding; 141 | final LogPrinter printer; 142 | IOSink _sink; 143 | 144 | FileOutput( 145 | {this.file, 146 | this.overrideExisting = false, 147 | this.encoding = utf8, 148 | this.printer = const SimplePrinter()}); 149 | 150 | @override 151 | void init() { 152 | _sink = file.openWrite( 153 | mode: overrideExisting ? FileMode.writeOnly : FileMode.writeOnlyAppend, 154 | encoding: encoding, 155 | ); 156 | } 157 | 158 | @override 159 | void output(LogMessage event) { 160 | printer.log(event).forEach((line) => _sink.write(line)); 161 | } 162 | 163 | @override 164 | void dispose() async { 165 | await _sink.flush(); 166 | await _sink.close(); 167 | } 168 | 169 | List readLogs() { 170 | return file.readAsLinesSync(encoding: encoding); 171 | } 172 | } 173 | 174 | class MemoryLogOutput implements LogOutput { 175 | /// Maximum events in [buffer]. 176 | final int bufferSize; 177 | 178 | /// The buffer of events. 179 | final ListQueue buffer; 180 | 181 | void Function() onUpdate; 182 | 183 | MemoryLogOutput({this.bufferSize = 40}) : buffer = ListQueue(bufferSize); 184 | 185 | @override 186 | void output(LogMessage event) { 187 | if (buffer.length == bufferSize) { 188 | buffer.removeFirst(); 189 | } 190 | buffer.add(event); 191 | if (onUpdate != null) onUpdate(); 192 | } 193 | 194 | @override 195 | void dispose() { 196 | buffer.clear(); 197 | } 198 | 199 | @override 200 | void init() {} 201 | } 202 | 203 | class StreamLogOutput implements LogOutput { 204 | StreamController _streamLog = StreamController.broadcast(); 205 | Stream get stream => _streamLog.stream; 206 | @override 207 | void dispose() { 208 | _streamLog.close(); 209 | _streamLog = null; 210 | } 211 | 212 | @override 213 | void init() {} 214 | 215 | @override 216 | void output(LogMessage event) { 217 | _streamLog.add(event); 218 | } 219 | } 220 | 221 | class ConsoleOutput implements LogOutput { 222 | @override 223 | void dispose() {} 224 | final LogPrinter printer; 225 | ConsoleOutput({this.printer = const SimplePrinter()}); 226 | @override 227 | void init() {} 228 | 229 | @override 230 | void output(LogMessage event) { 231 | printer.log(event).forEach(print); 232 | } 233 | } 234 | 235 | class Logger { 236 | Level level = Level.verbose; 237 | LogOutput logOutput; 238 | Logger({this.logOutput}) { 239 | logOutput ??= ConsoleOutput(); 240 | logOutput.init(); 241 | } 242 | 243 | /// Log a message at level [Level.verbose]. 244 | void v(dynamic message, [dynamic tag, StackTrace stackTrace]) { 245 | log(Level.verbose, message, 246 | tag: tag, stackTrace: stackTrace, includeTime: true); 247 | } 248 | 249 | /// Log a message at level [Level.debug]. 250 | void d(dynamic message, [dynamic tag, StackTrace stackTrace]) { 251 | log(Level.debug, message, 252 | tag: tag, stackTrace: stackTrace, includeTime: true); 253 | } 254 | 255 | /// Log a message at level [Level.info]. 256 | void i(dynamic message, [dynamic tag, StackTrace stackTrace]) { 257 | log(Level.info, message, 258 | tag: tag, stackTrace: stackTrace, includeTime: true); 259 | } 260 | 261 | /// Log a message at level [Level.warning]. 262 | void w(dynamic message, [dynamic tag, StackTrace stackTrace]) { 263 | log(Level.warning, message, 264 | tag: tag, stackTrace: stackTrace, includeTime: true); 265 | } 266 | 267 | /// Log a message at level [Level.error]. 268 | void e(dynamic message, [dynamic tag, StackTrace stackTrace]) { 269 | log(Level.error, message, 270 | tag: tag, stackTrace: stackTrace, includeTime: true); 271 | } 272 | 273 | /// Log a message at level [Level.critical]. 274 | void critical(dynamic message, [dynamic tag, StackTrace stackTrace]) { 275 | log(Level.critical, message, 276 | tag: tag, stackTrace: stackTrace, includeTime: true); 277 | } 278 | 279 | /// Log a message with [level]. 280 | void log(Level level, dynamic message, 281 | {String tag, StackTrace stackTrace, bool includeTime = false}) { 282 | LogMessage logMessage = LogMessage(); 283 | if (includeTime) { 284 | logMessage.time = DateTime.now(); 285 | } 286 | logMessage.message = message; 287 | logMessage.stackTrace = stackTrace; 288 | logMessage.logLevel = level; 289 | logMessage.title = tag; 290 | logOutput.output(logMessage); 291 | } 292 | 293 | void dispose() { 294 | logOutput.dispose(); 295 | } 296 | } 297 | -------------------------------------------------------------------------------- /lib/utils/utils.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'dart:io'; 3 | 4 | import 'dart:typed_data'; 5 | 6 | import 'package:flushbar/flushbar_helper.dart'; 7 | import 'package:flutter/material.dart'; 8 | import 'package:path_provider/path_provider.dart'; 9 | import 'package:rhasspy_mobile_app/main.dart'; 10 | import 'package:rhasspy_mobile_app/rhasspy_dart/rhasspy_api.dart'; 11 | import 'package:shared_preferences/shared_preferences.dart'; 12 | 13 | Uint8List waveHeader( 14 | int totalAudioLen, int sampleRate, int channels, int byteRate) { 15 | int totalDataLen = totalAudioLen + 36; 16 | Uint8List header = Uint8List(44); 17 | header[0] = ascii.encode("R").first; 18 | header[1] = ascii.encode("I").first; 19 | header[2] = ascii.encode("F").first; 20 | header[3] = ascii.encode("F").first; 21 | header[4] = (totalDataLen & 0xff); 22 | header[5] = ((totalDataLen >> 8) & 0xff); 23 | header[6] = ((totalDataLen >> 16) & 0xff); 24 | header[7] = ((totalDataLen >> 24) & 0xff); 25 | header[8] = ascii.encode("W").first; 26 | header[9] = ascii.encode("A").first; 27 | header[10] = ascii.encode("V").first; 28 | header[11] = ascii.encode("E").first; 29 | header[12] = ascii.encode("f").first; 30 | header[13] = ascii.encode("m").first; 31 | header[14] = ascii.encode("t").first; 32 | header[15] = ascii.encode(" ").first; 33 | header[16] = 16; 34 | header[17] = 0; 35 | header[18] = 0; 36 | header[19] = 0; 37 | header[20] = 1; 38 | header[21] = 0; 39 | header[22] = channels; 40 | header[23] = 0; 41 | header[24] = (sampleRate & 0xff); 42 | header[25] = ((sampleRate >> 8) & 0xff); 43 | header[26] = ((sampleRate >> 16) & 0xff); 44 | header[27] = ((sampleRate >> 24) & 0xff); 45 | header[28] = (byteRate & 0xff); 46 | header[29] = ((byteRate >> 8) & 0xff); 47 | header[30] = ((byteRate >> 16) & 0xff); 48 | header[31] = ((byteRate >> 24) & 0xff); 49 | header[32] = 1; 50 | header[33] = 0; 51 | header[34] = 16; 52 | header[35] = 0; 53 | header[36] = ascii.encode("d").first; 54 | header[37] = ascii.encode("a").first; 55 | header[38] = ascii.encode("t").first; 56 | header[39] = ascii.encode("a").first; 57 | header[40] = (totalAudioLen & 0xff); 58 | header[41] = ((totalAudioLen >> 8) & 0xff); 59 | header[42] = ((totalAudioLen >> 16) & 0xff); 60 | header[43] = ((totalAudioLen >> 24) & 0xff); 61 | return header; 62 | } 63 | 64 | Future applySettings(RhasspyApi rhasspy, RhasspyProfile profile) async { 65 | if (!profile.isDialogueRhasspy) { 66 | log.w("rhasspy Dialogue Management is not enable."); 67 | profile.setDialogueSystem("rhasspy"); 68 | } 69 | log.d("sending new profile config", "APP"); 70 | try { 71 | if (await rhasspy.setProfile(ProfileLayers.defaults, profile)) { 72 | log.i("Restarting rhasspy...", "RHASSPY"); 73 | if (await rhasspy.restart()) { 74 | log.i("restarted", "RHASSPY"); 75 | return true; 76 | } else { 77 | log.e("failed to restarted rhasspy", "RHASSPY"); 78 | return false; 79 | } 80 | } else { 81 | log.e("failed to send new profile", "RHASSPY"); 82 | return false; 83 | } 84 | } catch (e) { 85 | log.e("failed to send new profile ${e.toString()}"); 86 | return false; 87 | } 88 | } 89 | 90 | Future getRhasspyInstance(SharedPreferences prefs, 91 | [BuildContext context]) async { 92 | SecurityContext securityContext = SecurityContext.defaultContext; 93 | Directory appDocDirectory = await getApplicationDocumentsDirectory(); 94 | String certificatePath = appDocDirectory.path + "/SslCertificate.pem"; 95 | try { 96 | if (File(certificatePath).existsSync()) 97 | securityContext.setTrustedCertificates(certificatePath); 98 | } on TlsException {} 99 | String value = prefs.getString("Rhasspyip"); 100 | if (value == null) { 101 | if (context != null) 102 | FlushbarHelper.createError( 103 | message: "please insert rhasspy server ip first") 104 | .show(context); 105 | log.e("please insert rhasspy server ip first", "RHASSPY"); 106 | return null; 107 | } 108 | String ip = value.split(":").first; 109 | int port = int.parse(value.split(":").last); 110 | return RhasspyApi(ip, port, prefs.getBool("SSL") ?? false, 111 | securityContext: securityContext); 112 | } 113 | 114 | MaterialColor createMaterialColor(Color color) { 115 | List strengths = [.05]; 116 | final swatch = {}; 117 | final int r = color.red, g = color.green, b = color.blue; 118 | 119 | for (int i = 1; i < 10; i++) { 120 | strengths.add(0.1 * i); 121 | } 122 | strengths.forEach((strength) { 123 | final double ds = 0.5 - strength; 124 | swatch[(strength * 1000).round()] = Color.fromRGBO( 125 | r + ((ds < 0 ? r : (255 - r)) * ds).round(), 126 | g + ((ds < 0 ? g : (255 - g)) * ds).round(), 127 | b + ((ds < 0 ? b : (255 - b)) * ds).round(), 128 | 1, 129 | ); 130 | }); 131 | return MaterialColor(color.value, swatch); 132 | } 133 | -------------------------------------------------------------------------------- /lib/wake_word/udp_wake_word.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/foundation.dart'; 2 | import 'package:flutter/services.dart'; 3 | import 'package:rhasspy_mobile_app/wake_word/wake_word_base.dart'; 4 | 5 | class UdpWakeWord extends WakeWordBase { 6 | String ip; 7 | int port; 8 | UdpWakeWord(this.ip, this.port); 9 | @visibleForTesting 10 | static const MethodChannel channel = const MethodChannel('wake_word'); 11 | 12 | @override 13 | Future get isAvailable async { 14 | List availableWakeWordDetector = 15 | await super.availableWakeWordDetector; 16 | return availableWakeWordDetector.contains(name); 17 | } 18 | 19 | @override 20 | Future startListening() { 21 | return channel.invokeMethod( 22 | "start", {"ip": this.ip, "port": this.port, "wakeWordDetector": name}); 23 | } 24 | 25 | @override 26 | Future stopListening() { 27 | return channel.invokeMethod("stop"); 28 | } 29 | 30 | @override 31 | String name = "UDP"; 32 | } 33 | -------------------------------------------------------------------------------- /lib/wake_word/wake_word_base.dart: -------------------------------------------------------------------------------- 1 | import 'package:rhasspy_mobile_app/wake_word/wake_word_utils.dart'; 2 | 3 | abstract class WakeWordBase extends WakeWordUtils { 4 | String name; 5 | Future get isAvailable; 6 | Future startListening(); 7 | Future stopListening(); 8 | } 9 | -------------------------------------------------------------------------------- /lib/wake_word/wake_word_utils.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/services.dart'; 2 | 3 | class WakeWordUtils { 4 | static const MethodChannel _channel = const MethodChannel('wake_word'); 5 | 6 | Future get isRunning { 7 | return _channel.invokeMethod("isRunning"); 8 | } 9 | 10 | Future get isListening { 11 | return _channel.invokeMethod("isListening"); 12 | } 13 | 14 | Future> get availableWakeWordDetector async { 15 | return List.castFrom( 16 | (await _channel.invokeMethod("getWakeWordDetector"))); 17 | } 18 | 19 | Future pause() { 20 | return _channel.invokeMethod("pause"); 21 | } 22 | 23 | Future resume() { 24 | return _channel.invokeMethod("resume"); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /lib/widget/Intent_viewer.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:rhasspy_mobile_app/rhasspy_dart/parse_messages.dart'; 3 | 4 | class IntentViewer extends StatefulWidget { 5 | final NluIntentParsed intent; 6 | IntentViewer(this.intent, {Key key}) : super(key: key); 7 | 8 | @override 9 | _IntentViewerState createState() => _IntentViewerState(); 10 | } 11 | 12 | class _IntentViewerState extends State { 13 | @override 14 | Widget build(BuildContext context) { 15 | if (widget.intent != null) { 16 | return Column( 17 | children: [ 18 | Padding( 19 | padding: const EdgeInsets.all(8.0), 20 | child: Container( 21 | decoration: BoxDecoration( 22 | color: Colors.blue, 23 | border: Border.all(width: 0.5), 24 | borderRadius: BorderRadius.circular(5)), 25 | child: Padding( 26 | padding: const EdgeInsets.all(4), 27 | child: Text( 28 | widget.intent.intent.intentName, 29 | style: TextStyle( 30 | color: Colors.white, 31 | ), 32 | textScaleFactor: 1.5, 33 | ), 34 | )), 35 | ), 36 | ListView.builder( 37 | itemBuilder: (context, index) { 38 | Slots slot = widget.intent.slots.elementAt(index); 39 | // Text.rich(textSpan) 40 | return Padding( 41 | padding: const EdgeInsets.all(8.0), 42 | child: Row( 43 | children: [ 44 | Padding( 45 | padding: const EdgeInsets.symmetric(horizontal: 4), 46 | child: Text( 47 | slot.value.value, 48 | textScaleFactor: 1.1, 49 | style: TextStyle(), 50 | ), 51 | ), 52 | Container( 53 | // color: Colors.red, 54 | decoration: BoxDecoration( 55 | color: Colors.blue, 56 | border: Border.all(width: 0.5), 57 | borderRadius: BorderRadius.circular(5)), 58 | child: Padding( 59 | padding: const EdgeInsets.all(2), 60 | child: Text( 61 | slot.slotName, 62 | style: TextStyle( 63 | color: Colors.white, 64 | ), 65 | textScaleFactor: 1.5, 66 | ), 67 | )), 68 | ], 69 | ), 70 | ); 71 | }, 72 | itemCount: widget.intent.slots.length, 73 | physics: NeverScrollableScrollPhysics(), 74 | shrinkWrap: true, 75 | ), 76 | ], 77 | ); 78 | } 79 | return Container(); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /pubspec.lock: -------------------------------------------------------------------------------- 1 | # Generated by pub 2 | # See https://dart.dev/tools/pub/glossary#lockfile 3 | packages: 4 | _fe_analyzer_shared: 5 | dependency: transitive 6 | description: 7 | name: _fe_analyzer_shared 8 | url: "https://pub.dartlang.org" 9 | source: hosted 10 | version: "12.0.0" 11 | analyzer: 12 | dependency: transitive 13 | description: 14 | name: analyzer 15 | url: "https://pub.dartlang.org" 16 | source: hosted 17 | version: "0.40.6" 18 | archive: 19 | dependency: transitive 20 | description: 21 | name: archive 22 | url: "https://pub.dartlang.org" 23 | source: hosted 24 | version: "2.0.13" 25 | args: 26 | dependency: transitive 27 | description: 28 | name: args 29 | url: "https://pub.dartlang.org" 30 | source: hosted 31 | version: "1.6.0" 32 | async: 33 | dependency: transitive 34 | description: 35 | name: async 36 | url: "https://pub.dartlang.org" 37 | source: hosted 38 | version: "2.5.0-nullsafety.1" 39 | audioplayers: 40 | dependency: "direct main" 41 | description: 42 | name: audioplayers 43 | url: "https://pub.dartlang.org" 44 | source: hosted 45 | version: "0.17.0" 46 | boolean_selector: 47 | dependency: transitive 48 | description: 49 | name: boolean_selector 50 | url: "https://pub.dartlang.org" 51 | source: hosted 52 | version: "2.1.0-nullsafety.1" 53 | build: 54 | dependency: transitive 55 | description: 56 | name: build 57 | url: "https://pub.dartlang.org" 58 | source: hosted 59 | version: "1.6.0" 60 | built_collection: 61 | dependency: transitive 62 | description: 63 | name: built_collection 64 | url: "https://pub.dartlang.org" 65 | source: hosted 66 | version: "4.3.2" 67 | built_value: 68 | dependency: transitive 69 | description: 70 | name: built_value 71 | url: "https://pub.dartlang.org" 72 | source: hosted 73 | version: "7.1.0" 74 | characters: 75 | dependency: transitive 76 | description: 77 | name: characters 78 | url: "https://pub.dartlang.org" 79 | source: hosted 80 | version: "1.1.0-nullsafety.3" 81 | charcode: 82 | dependency: transitive 83 | description: 84 | name: charcode 85 | url: "https://pub.dartlang.org" 86 | source: hosted 87 | version: "1.2.0-nullsafety.1" 88 | cli_util: 89 | dependency: transitive 90 | description: 91 | name: cli_util 92 | url: "https://pub.dartlang.org" 93 | source: hosted 94 | version: "0.2.0" 95 | clock: 96 | dependency: transitive 97 | description: 98 | name: clock 99 | url: "https://pub.dartlang.org" 100 | source: hosted 101 | version: "1.1.0-nullsafety.1" 102 | code_builder: 103 | dependency: transitive 104 | description: 105 | name: code_builder 106 | url: "https://pub.dartlang.org" 107 | source: hosted 108 | version: "3.5.0" 109 | collection: 110 | dependency: transitive 111 | description: 112 | name: collection 113 | url: "https://pub.dartlang.org" 114 | source: hosted 115 | version: "1.15.0-nullsafety.3" 116 | convert: 117 | dependency: transitive 118 | description: 119 | name: convert 120 | url: "https://pub.dartlang.org" 121 | source: hosted 122 | version: "2.1.1" 123 | crypto: 124 | dependency: transitive 125 | description: 126 | name: crypto 127 | url: "https://pub.dartlang.org" 128 | source: hosted 129 | version: "2.1.4" 130 | dart_style: 131 | dependency: transitive 132 | description: 133 | name: dart_style 134 | url: "https://pub.dartlang.org" 135 | source: hosted 136 | version: "1.3.10" 137 | dio: 138 | dependency: "direct main" 139 | description: 140 | name: dio 141 | url: "https://pub.dartlang.org" 142 | source: hosted 143 | version: "3.0.10" 144 | event_bus: 145 | dependency: transitive 146 | description: 147 | name: event_bus 148 | url: "https://pub.dartlang.org" 149 | source: hosted 150 | version: "1.1.1" 151 | fake_async: 152 | dependency: transitive 153 | description: 154 | name: fake_async 155 | url: "https://pub.dartlang.org" 156 | source: hosted 157 | version: "1.2.0-nullsafety.1" 158 | ffi: 159 | dependency: transitive 160 | description: 161 | name: ffi 162 | url: "https://pub.dartlang.org" 163 | source: hosted 164 | version: "0.1.3" 165 | file: 166 | dependency: transitive 167 | description: 168 | name: file 169 | url: "https://pub.dartlang.org" 170 | source: hosted 171 | version: "5.2.0" 172 | file_picker: 173 | dependency: "direct main" 174 | description: 175 | name: file_picker 176 | url: "https://pub.dartlang.org" 177 | source: hosted 178 | version: "2.0.13" 179 | fixnum: 180 | dependency: transitive 181 | description: 182 | name: fixnum 183 | url: "https://pub.dartlang.org" 184 | source: hosted 185 | version: "0.10.11" 186 | flushbar: 187 | dependency: "direct main" 188 | description: 189 | name: flushbar 190 | url: "https://pub.dartlang.org" 191 | source: hosted 192 | version: "1.10.4" 193 | flutter: 194 | dependency: "direct main" 195 | description: flutter 196 | source: sdk 197 | version: "0.0.0" 198 | flutter_audio_recorder: 199 | dependency: "direct main" 200 | description: 201 | name: flutter_audio_recorder 202 | url: "https://pub.dartlang.org" 203 | source: hosted 204 | version: "0.5.5" 205 | flutter_launcher_icons: 206 | dependency: "direct dev" 207 | description: 208 | name: flutter_launcher_icons 209 | url: "https://pub.dartlang.org" 210 | source: hosted 211 | version: "0.7.5" 212 | flutter_local_notifications: 213 | dependency: "direct main" 214 | description: 215 | name: flutter_local_notifications 216 | url: "https://pub.dartlang.org" 217 | source: hosted 218 | version: "3.0.1" 219 | flutter_local_notifications_platform_interface: 220 | dependency: transitive 221 | description: 222 | name: flutter_local_notifications_platform_interface 223 | url: "https://pub.dartlang.org" 224 | source: hosted 225 | version: "2.0.0" 226 | flutter_plugin_android_lifecycle: 227 | dependency: transitive 228 | description: 229 | name: flutter_plugin_android_lifecycle 230 | url: "https://pub.dartlang.org" 231 | source: hosted 232 | version: "1.0.8" 233 | flutter_test: 234 | dependency: "direct dev" 235 | description: flutter 236 | source: sdk 237 | version: "0.0.0" 238 | flutter_web_plugins: 239 | dependency: transitive 240 | description: flutter 241 | source: sdk 242 | version: "0.0.0" 243 | glob: 244 | dependency: transitive 245 | description: 246 | name: glob 247 | url: "https://pub.dartlang.org" 248 | source: hosted 249 | version: "1.2.0" 250 | http_mock_adapter: 251 | dependency: "direct dev" 252 | description: 253 | name: http_mock_adapter 254 | url: "https://pub.dartlang.org" 255 | source: hosted 256 | version: "0.1.3" 257 | http_parser: 258 | dependency: transitive 259 | description: 260 | name: http_parser 261 | url: "https://pub.dartlang.org" 262 | source: hosted 263 | version: "3.1.4" 264 | image: 265 | dependency: transitive 266 | description: 267 | name: image 268 | url: "https://pub.dartlang.org" 269 | source: hosted 270 | version: "2.1.19" 271 | intl: 272 | dependency: transitive 273 | description: 274 | name: intl 275 | url: "https://pub.dartlang.org" 276 | source: hosted 277 | version: "0.16.1" 278 | js: 279 | dependency: transitive 280 | description: 281 | name: js 282 | url: "https://pub.dartlang.org" 283 | source: hosted 284 | version: "0.6.3-nullsafety.2" 285 | logging: 286 | dependency: transitive 287 | description: 288 | name: logging 289 | url: "https://pub.dartlang.org" 290 | source: hosted 291 | version: "0.11.4" 292 | matcher: 293 | dependency: transitive 294 | description: 295 | name: matcher 296 | url: "https://pub.dartlang.org" 297 | source: hosted 298 | version: "0.12.10-nullsafety.1" 299 | material_design_icons_flutter: 300 | dependency: "direct main" 301 | description: 302 | name: material_design_icons_flutter 303 | url: "https://pub.dartlang.org" 304 | source: hosted 305 | version: "4.0.5755" 306 | meta: 307 | dependency: transitive 308 | description: 309 | name: meta 310 | url: "https://pub.dartlang.org" 311 | source: hosted 312 | version: "1.3.0-nullsafety.3" 313 | mockito: 314 | dependency: "direct dev" 315 | description: 316 | name: mockito 317 | url: "https://pub.dartlang.org" 318 | source: hosted 319 | version: "4.1.3" 320 | mqtt_client: 321 | dependency: "direct main" 322 | description: 323 | name: mqtt_client 324 | url: "https://pub.dartlang.org" 325 | source: hosted 326 | version: "8.1.0" 327 | nested: 328 | dependency: transitive 329 | description: 330 | name: nested 331 | url: "https://pub.dartlang.org" 332 | source: hosted 333 | version: "0.0.4" 334 | node_interop: 335 | dependency: transitive 336 | description: 337 | name: node_interop 338 | url: "https://pub.dartlang.org" 339 | source: hosted 340 | version: "1.2.1" 341 | node_io: 342 | dependency: transitive 343 | description: 344 | name: node_io 345 | url: "https://pub.dartlang.org" 346 | source: hosted 347 | version: "1.2.0" 348 | package_config: 349 | dependency: transitive 350 | description: 351 | name: package_config 352 | url: "https://pub.dartlang.org" 353 | source: hosted 354 | version: "1.9.3" 355 | path: 356 | dependency: transitive 357 | description: 358 | name: path 359 | url: "https://pub.dartlang.org" 360 | source: hosted 361 | version: "1.8.0-nullsafety.1" 362 | path_provider: 363 | dependency: "direct main" 364 | description: 365 | name: path_provider 366 | url: "https://pub.dartlang.org" 367 | source: hosted 368 | version: "1.6.24" 369 | path_provider_linux: 370 | dependency: transitive 371 | description: 372 | name: path_provider_linux 373 | url: "https://pub.dartlang.org" 374 | source: hosted 375 | version: "0.0.1+1" 376 | path_provider_macos: 377 | dependency: transitive 378 | description: 379 | name: path_provider_macos 380 | url: "https://pub.dartlang.org" 381 | source: hosted 382 | version: "0.0.4+3" 383 | path_provider_platform_interface: 384 | dependency: transitive 385 | description: 386 | name: path_provider_platform_interface 387 | url: "https://pub.dartlang.org" 388 | source: hosted 389 | version: "1.0.3" 390 | path_provider_windows: 391 | dependency: transitive 392 | description: 393 | name: path_provider_windows 394 | url: "https://pub.dartlang.org" 395 | source: hosted 396 | version: "0.0.4+1" 397 | pedantic: 398 | dependency: transitive 399 | description: 400 | name: pedantic 401 | url: "https://pub.dartlang.org" 402 | source: hosted 403 | version: "1.10.0-nullsafety.2" 404 | permission_handler: 405 | dependency: "direct main" 406 | description: 407 | name: permission_handler 408 | url: "https://pub.dartlang.org" 409 | source: hosted 410 | version: "5.0.1+1" 411 | permission_handler_platform_interface: 412 | dependency: transitive 413 | description: 414 | name: permission_handler_platform_interface 415 | url: "https://pub.dartlang.org" 416 | source: hosted 417 | version: "2.0.1" 418 | petitparser: 419 | dependency: transitive 420 | description: 421 | name: petitparser 422 | url: "https://pub.dartlang.org" 423 | source: hosted 424 | version: "3.1.0" 425 | platform: 426 | dependency: transitive 427 | description: 428 | name: platform 429 | url: "https://pub.dartlang.org" 430 | source: hosted 431 | version: "2.2.1" 432 | plugin_platform_interface: 433 | dependency: transitive 434 | description: 435 | name: plugin_platform_interface 436 | url: "https://pub.dartlang.org" 437 | source: hosted 438 | version: "1.0.2" 439 | process: 440 | dependency: transitive 441 | description: 442 | name: process 443 | url: "https://pub.dartlang.org" 444 | source: hosted 445 | version: "3.0.13" 446 | provider: 447 | dependency: "direct main" 448 | description: 449 | name: provider 450 | url: "https://pub.dartlang.org" 451 | source: hosted 452 | version: "4.3.2+2" 453 | pub_semver: 454 | dependency: transitive 455 | description: 456 | name: pub_semver 457 | url: "https://pub.dartlang.org" 458 | source: hosted 459 | version: "1.4.4" 460 | quiver: 461 | dependency: transitive 462 | description: 463 | name: quiver 464 | url: "https://pub.dartlang.org" 465 | source: hosted 466 | version: "2.1.5" 467 | shared_preferences: 468 | dependency: "direct main" 469 | description: 470 | name: shared_preferences 471 | url: "https://pub.dartlang.org" 472 | source: hosted 473 | version: "0.5.12+2" 474 | shared_preferences_linux: 475 | dependency: transitive 476 | description: 477 | name: shared_preferences_linux 478 | url: "https://pub.dartlang.org" 479 | source: hosted 480 | version: "0.0.2+1" 481 | shared_preferences_macos: 482 | dependency: transitive 483 | description: 484 | name: shared_preferences_macos 485 | url: "https://pub.dartlang.org" 486 | source: hosted 487 | version: "0.0.1+10" 488 | shared_preferences_platform_interface: 489 | dependency: transitive 490 | description: 491 | name: shared_preferences_platform_interface 492 | url: "https://pub.dartlang.org" 493 | source: hosted 494 | version: "1.0.4" 495 | shared_preferences_web: 496 | dependency: transitive 497 | description: 498 | name: shared_preferences_web 499 | url: "https://pub.dartlang.org" 500 | source: hosted 501 | version: "0.1.2+7" 502 | shared_preferences_windows: 503 | dependency: transitive 504 | description: 505 | name: shared_preferences_windows 506 | url: "https://pub.dartlang.org" 507 | source: hosted 508 | version: "0.0.1+1" 509 | sky_engine: 510 | dependency: transitive 511 | description: flutter 512 | source: sdk 513 | version: "0.0.99" 514 | source_gen: 515 | dependency: transitive 516 | description: 517 | name: source_gen 518 | url: "https://pub.dartlang.org" 519 | source: hosted 520 | version: "0.9.10+1" 521 | source_span: 522 | dependency: transitive 523 | description: 524 | name: source_span 525 | url: "https://pub.dartlang.org" 526 | source: hosted 527 | version: "1.8.0-nullsafety.2" 528 | stack_trace: 529 | dependency: transitive 530 | description: 531 | name: stack_trace 532 | url: "https://pub.dartlang.org" 533 | source: hosted 534 | version: "1.10.0-nullsafety.1" 535 | stream_channel: 536 | dependency: transitive 537 | description: 538 | name: stream_channel 539 | url: "https://pub.dartlang.org" 540 | source: hosted 541 | version: "2.1.0-nullsafety.1" 542 | string_scanner: 543 | dependency: transitive 544 | description: 545 | name: string_scanner 546 | url: "https://pub.dartlang.org" 547 | source: hosted 548 | version: "1.1.0-nullsafety.1" 549 | term_glyph: 550 | dependency: transitive 551 | description: 552 | name: term_glyph 553 | url: "https://pub.dartlang.org" 554 | source: hosted 555 | version: "1.2.0-nullsafety.1" 556 | test_api: 557 | dependency: transitive 558 | description: 559 | name: test_api 560 | url: "https://pub.dartlang.org" 561 | source: hosted 562 | version: "0.2.19-nullsafety.2" 563 | timezone: 564 | dependency: transitive 565 | description: 566 | name: timezone 567 | url: "https://pub.dartlang.org" 568 | source: hosted 569 | version: "0.5.9" 570 | typed_data: 571 | dependency: transitive 572 | description: 573 | name: typed_data 574 | url: "https://pub.dartlang.org" 575 | source: hosted 576 | version: "1.3.0-nullsafety.3" 577 | uuid: 578 | dependency: transitive 579 | description: 580 | name: uuid 581 | url: "https://pub.dartlang.org" 582 | source: hosted 583 | version: "2.2.2" 584 | vector_math: 585 | dependency: transitive 586 | description: 587 | name: vector_math 588 | url: "https://pub.dartlang.org" 589 | source: hosted 590 | version: "2.1.0-nullsafety.3" 591 | vibration: 592 | dependency: "direct main" 593 | description: 594 | name: vibration 595 | url: "https://pub.dartlang.org" 596 | source: hosted 597 | version: "1.7.3" 598 | vibration_web: 599 | dependency: transitive 600 | description: 601 | name: vibration_web 602 | url: "https://pub.dartlang.org" 603 | source: hosted 604 | version: "1.6.2" 605 | watcher: 606 | dependency: transitive 607 | description: 608 | name: watcher 609 | url: "https://pub.dartlang.org" 610 | source: hosted 611 | version: "0.9.7+15" 612 | win32: 613 | dependency: transitive 614 | description: 615 | name: win32 616 | url: "https://pub.dartlang.org" 617 | source: hosted 618 | version: "1.7.3" 619 | xdg_directories: 620 | dependency: transitive 621 | description: 622 | name: xdg_directories 623 | url: "https://pub.dartlang.org" 624 | source: hosted 625 | version: "0.1.0" 626 | xml: 627 | dependency: transitive 628 | description: 629 | name: xml 630 | url: "https://pub.dartlang.org" 631 | source: hosted 632 | version: "4.5.1" 633 | yaml: 634 | dependency: transitive 635 | description: 636 | name: yaml 637 | url: "https://pub.dartlang.org" 638 | source: hosted 639 | version: "2.2.1" 640 | sdks: 641 | dart: ">=2.10.0 <2.11.0" 642 | flutter: ">=1.20.0 <2.0.0" 643 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: rhasspy_mobile_app 2 | description: A simple app to interact with rhasspy. 3 | 4 | version: 1.7.0+1 5 | environment: 6 | sdk: ">=2.7.0 <3.0.0" 7 | 8 | dependencies: 9 | flutter: 10 | sdk: flutter 11 | shared_preferences: ^0.5.12+2 12 | permission_handler: ^5.0.1+1 13 | flutter_audio_recorder: ^0.5.5 14 | path_provider: 15 | file_picker: ^2.0.13 16 | audioplayers: ^0.17.0 17 | dio: ^3.0.10 18 | flushbar: ^1.10.4 19 | mqtt_client: ^8.1.0 20 | provider: ^4.3.2+2 21 | flutter_local_notifications: ^3.0.1 22 | material_design_icons_flutter: ^4.0.5755 23 | vibration: ^1.7.3 24 | 25 | dev_dependencies: 26 | flutter_test: 27 | sdk: flutter 28 | mockito: ^4.1.1 29 | http_mock_adapter: ^0.1.3 30 | flutter_launcher_icons: ^0.7.4 31 | 32 | flutter: 33 | uses-material-design: true 34 | assets: 35 | - assets/images/ 36 | 37 | flutter_icons: 38 | image_path: 'assets/images/rhasspy.png' 39 | android: true 40 | ios: true 41 | -------------------------------------------------------------------------------- /test/rhasspy_api_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'dart:io'; 3 | import 'dart:typed_data'; 4 | import 'package:dio/dio.dart'; 5 | import 'package:http_mock_adapter/http_mock_adapter.dart'; 6 | import 'package:flutter_test/flutter_test.dart'; 7 | import 'package:mockito/mockito.dart'; 8 | import 'package:rhasspy_mobile_app/rhasspy_dart/rhasspy_api.dart'; 9 | 10 | main() { 11 | test("test adding siteId with mqtt enabled", () async { 12 | DioAdapterMockito dioAdapterMockito = DioAdapterMockito(); 13 | RhasspyApi rhasspyApi = RhasspyApi("127.0.0.1", 12101, false); 14 | expect(rhasspyApi.baseUrl, "http://127.0.0.1:12101"); 15 | rhasspyApi.dio.httpClientAdapter = dioAdapterMockito; 16 | final responsePayload = jsonEncode( 17 | { 18 | "mqtt": { 19 | "enabled": true, 20 | "host": "127.0.0.1", 21 | "password": "pass", 22 | "site_id": "default", 23 | "username": "username" 24 | }, 25 | }, 26 | ); 27 | 28 | final responseBody = ResponseBody.fromString( 29 | responsePayload, 30 | 200, 31 | headers: { 32 | Headers.contentTypeHeader: [Headers.jsonContentType], 33 | }, 34 | ); 35 | when(dioAdapterMockito.fetch(any, any, any)) 36 | .thenAnswer((_) async => responseBody); 37 | var profile = RhasspyProfile.fromJson( 38 | await rhasspyApi.getProfile(ProfileLayers.defaults)); 39 | expect(profile.isMqttEnable, true); 40 | expect(profile.mqttSettings.host, "127.0.0.1"); 41 | expect(profile.mqttSettings.password, "pass"); 42 | expect(profile.mqttSettings.username, "username"); 43 | expect(profile.mqttSettings.siteIds, ["default"]); 44 | expect(profile.isDialogueRhasspy, false); 45 | expect(profile.containsSiteId("default"), true); 46 | profile.addSiteId("testSiteId"); 47 | expect(profile.siteIds, ["default", "testSiteId"]); 48 | expect(profile.containsSiteId("testSiteId"), true); 49 | expect( 50 | profile.toJson(), 51 | { 52 | "mqtt": { 53 | "enabled": true, 54 | "host": "127.0.0.1", 55 | "password": "pass", 56 | "site_id": "default,testSiteId", 57 | "username": "username" 58 | }, 59 | }, 60 | ); 61 | }); 62 | test("test adding siteId with mqtt enabled and a empty siteId", () async { 63 | DioAdapterMockito dioAdapterMockito = DioAdapterMockito(); 64 | RhasspyApi rhasspyApi = RhasspyApi("127.0.0.1", 12101, false); 65 | expect(rhasspyApi.baseUrl, "http://127.0.0.1:12101"); 66 | rhasspyApi.dio.httpClientAdapter = dioAdapterMockito; 67 | final responsePayload = jsonEncode( 68 | { 69 | "dialogue": {"system": "rhasspy"}, 70 | "mqtt": { 71 | "enabled": true, 72 | "host": "localhost", 73 | "password": "", 74 | "site_id": "", 75 | "username": "" 76 | }, 77 | }, 78 | ); 79 | 80 | final responseBody = ResponseBody.fromString( 81 | responsePayload, 82 | 200, 83 | headers: { 84 | Headers.contentTypeHeader: [Headers.jsonContentType], 85 | }, 86 | ); 87 | when(dioAdapterMockito.fetch(any, any, any)) 88 | .thenAnswer((_) async => responseBody); 89 | var profile = RhasspyProfile.fromJson( 90 | await rhasspyApi.getProfile(ProfileLayers.defaults)); 91 | expect(profile.isMqttEnable, true); 92 | expect(profile.mqttSettings.host, "localhost"); 93 | expect(profile.mqttSettings.password, ""); 94 | expect(profile.mqttSettings.username, ""); 95 | expect(profile.mqttSettings.siteIds, []); 96 | expect(profile.isDialogueRhasspy, true); 97 | expect(profile.containsSiteId("default"), false); 98 | profile.addSiteId("testId"); 99 | expect(profile.containsSiteId("testId"), true); 100 | expect( 101 | profile.toJson(), 102 | { 103 | "dialogue": {"system": "rhasspy"}, 104 | "mqtt": { 105 | "enabled": true, 106 | "host": "localhost", 107 | "password": "", 108 | "site_id": "testId", 109 | "username": "" 110 | }, 111 | }, 112 | ); 113 | }); 114 | test("test adding siteId and mqtt credentials", () async { 115 | DioAdapterMockito dioAdapterMockito = DioAdapterMockito(); 116 | RhasspyApi rhasspyApi = RhasspyApi("127.0.0.1", 12101, false); 117 | expect(rhasspyApi.baseUrl, "http://127.0.0.1:12101"); 118 | rhasspyApi.dio.httpClientAdapter = dioAdapterMockito; 119 | final responsePayload = jsonEncode( 120 | { 121 | "dialogue": {"system": "rhasspy"}, 122 | }, 123 | ); 124 | 125 | final responseBody = ResponseBody.fromString( 126 | responsePayload, 127 | 200, 128 | headers: { 129 | Headers.contentTypeHeader: [Headers.jsonContentType], 130 | }, 131 | ); 132 | when(dioAdapterMockito.fetch(any, any, any)) 133 | .thenAnswer((_) async => responseBody); 134 | var profile = RhasspyProfile.fromJson( 135 | await rhasspyApi.getProfile(ProfileLayers.defaults)); 136 | expect(profile.isMqttEnable, false); 137 | expect(profile.mqttSettings.host, ""); 138 | expect(profile.mqttSettings.password, ""); 139 | expect(profile.mqttSettings.username, ""); 140 | expect(profile.mqttSettings.siteIds, []); 141 | expect(profile.isDialogueRhasspy, true); 142 | expect(profile.containsSiteId("default"), false); 143 | profile.addSiteId("testId"); 144 | expect(profile.containsSiteId("testId"), true); 145 | profile.mqttSettings.password = "password"; 146 | profile.mqttSettings.port = 1883; 147 | profile.mqttSettings.username = "username"; 148 | profile.mqttSettings.host = "localhost"; 149 | profile.mqttSettings.enabled = true; 150 | expect( 151 | profile.toJson(), 152 | { 153 | "dialogue": {"system": "rhasspy"}, 154 | "mqtt": { 155 | "enabled": true, 156 | "host": "localhost", 157 | "password": "password", 158 | "site_id": "testId", 159 | "username": "username" 160 | }, 161 | }, 162 | ); 163 | }); 164 | test("test adding siteId and mqtt credentials with a empty profile", 165 | () async { 166 | DioAdapterMockito dioAdapterMockito = DioAdapterMockito(); 167 | RhasspyApi rhasspyApi = RhasspyApi("127.0.0.1", 12101, false); 168 | expect(rhasspyApi.baseUrl, "http://127.0.0.1:12101"); 169 | rhasspyApi.dio.httpClientAdapter = dioAdapterMockito; 170 | final responsePayload = jsonEncode( 171 | {}, 172 | ); 173 | final responseBody = ResponseBody.fromString( 174 | responsePayload, 175 | 200, 176 | headers: { 177 | Headers.contentTypeHeader: [Headers.jsonContentType], 178 | }, 179 | ); 180 | when(dioAdapterMockito.fetch(any, any, any)) 181 | .thenAnswer((_) async => responseBody); 182 | var profile = RhasspyProfile.fromJson( 183 | await rhasspyApi.getProfile(ProfileLayers.defaults)); 184 | expect(profile.isMqttEnable, false); 185 | expect(profile.mqttSettings.host, ""); 186 | expect(profile.mqttSettings.password, ""); 187 | expect(profile.mqttSettings.username, ""); 188 | expect(profile.mqttSettings.siteIds, []); 189 | expect(profile.isDialogueRhasspy, false); 190 | expect(profile.containsSiteId("default"), false); 191 | profile.addSiteId("testId"); 192 | expect(profile.containsSiteId("testId"), true); 193 | profile.mqttSettings.password = "password"; 194 | profile.mqttSettings.port = 1883; 195 | profile.mqttSettings.username = "username"; 196 | profile.mqttSettings.host = "localhost"; 197 | profile.mqttSettings.enabled = true; 198 | profile.setDialogueSystem("rhasspy"); 199 | expect( 200 | profile.toJson(), 201 | { 202 | "dialogue": {"system": "rhasspy"}, 203 | "mqtt": { 204 | "enabled": true, 205 | "host": "localhost", 206 | "password": "password", 207 | "site_id": "testId", 208 | "username": "username" 209 | }, 210 | }, 211 | ); 212 | }); 213 | } 214 | -------------------------------------------------------------------------------- /test/rhasspy_mqtt_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:io'; 3 | 4 | import 'package:flutter_test/flutter_test.dart'; 5 | import 'package:mockito/mockito.dart'; 6 | import 'package:mqtt_client/mqtt_client.dart'; 7 | import 'package:mqtt_client/mqtt_server_client.dart'; 8 | import 'package:rhasspy_mobile_app/rhasspy_dart/rhasspy_mqtt_api.dart'; 9 | 10 | class MockClient extends Mock implements MqttServerClient {} 11 | 12 | main() { 13 | MockClient mockClient = MockClient(); 14 | group("test connection", () { 15 | test("code 0", () async { 16 | RhasspyMqttApi rhasspyMqttApi = RhasspyMqttApi( 17 | "host", 1883, false, "username", "password", "siteId", 18 | client: mockClient); 19 | MqttClientConnectionStatus connectionStatus = 20 | MqttClientConnectionStatus(); 21 | connectionStatus.state = MqttConnectionState.connected; 22 | connectionStatus.disconnectionOrigin = MqttDisconnectionOrigin.none; 23 | connectionStatus.returnCode = MqttConnectReturnCode.connectionAccepted; 24 | StreamController>> 25 | messagesController = StreamController(); 26 | when(mockClient.updates).thenAnswer((_) => messagesController.stream); 27 | rhasspyMqttApi.connected.then((value) { 28 | expect(value, true); 29 | }); 30 | when(mockClient.connect(any, any)) 31 | .thenAnswer((_) => Future.value(connectionStatus)); 32 | when(mockClient.connectionStatus).thenReturn(connectionStatus); 33 | expect(await rhasspyMqttApi.connect(), 0); 34 | expect(rhasspyMqttApi.isConnected, true); 35 | reset(mockClient); 36 | messagesController.close(); 37 | }); 38 | test("code 1", () async { 39 | RhasspyMqttApi rhasspyMqttApi = 40 | RhasspyMqttApi("host", 1883, false, "username", "password", "siteId"); 41 | rhasspyMqttApi.client = mockClient; 42 | MqttClientConnectionStatus connectionStatus = 43 | MqttClientConnectionStatus(); 44 | connectionStatus.state = MqttConnectionState.disconnected; 45 | connectionStatus.disconnectionOrigin = MqttDisconnectionOrigin.solicited; 46 | connectionStatus.returnCode = MqttConnectReturnCode.noneSpecified; 47 | when(mockClient.connect()) 48 | .thenAnswer((_) => Future.value(connectionStatus)); 49 | when(mockClient.connectionStatus).thenReturn(connectionStatus); 50 | rhasspyMqttApi.connected.then((value) { 51 | expect(value, false); 52 | }); 53 | expect(await rhasspyMqttApi.connect(), 1); 54 | expect(rhasspyMqttApi.isConnected, false); 55 | verifyNever(mockClient.subscribe(any, any)); 56 | reset(mockClient); 57 | }); 58 | test("code 2", () async { 59 | RhasspyMqttApi rhasspyMqttApi = 60 | RhasspyMqttApi("host", 1883, false, "username", "password", "siteId"); 61 | rhasspyMqttApi.client = mockClient; 62 | MqttClientConnectionStatus connectionStatus = 63 | MqttClientConnectionStatus(); 64 | connectionStatus.state = MqttConnectionState.disconnected; 65 | connectionStatus.disconnectionOrigin = 66 | MqttDisconnectionOrigin.unsolicited; 67 | connectionStatus.returnCode = MqttConnectReturnCode.badUsernameOrPassword; 68 | when(mockClient.connect()) 69 | .thenAnswer((_) => Future.value(connectionStatus)); 70 | 71 | when(mockClient.connectionStatus).thenReturn(connectionStatus); 72 | rhasspyMqttApi.connected.then((value) { 73 | expect(value, false); 74 | }); 75 | expect(await rhasspyMqttApi.connect(), 2); 76 | expect(rhasspyMqttApi.isConnected, false); 77 | verifyNever(mockClient.subscribe(any, any)); 78 | reset(mockClient); 79 | }); 80 | test("code 3", () async { 81 | RhasspyMqttApi rhasspyMqttApi = 82 | RhasspyMqttApi("host", 1883, false, "username", "password", "siteId"); 83 | rhasspyMqttApi.client = mockClient; 84 | MqttClientConnectionStatus connectionStatus = 85 | MqttClientConnectionStatus(); 86 | connectionStatus.state = MqttConnectionState.disconnected; 87 | connectionStatus.disconnectionOrigin = MqttDisconnectionOrigin.solicited; 88 | connectionStatus.returnCode = MqttConnectReturnCode.noneSpecified; 89 | when(mockClient.connect(any, any)).thenThrow(HandshakeException()); 90 | when(mockClient.connectionStatus).thenReturn(connectionStatus); 91 | when(mockClient.connectionStatus).thenReturn(connectionStatus); 92 | rhasspyMqttApi.connected.then((value) { 93 | expect(value, false); 94 | }); 95 | expect(await rhasspyMqttApi.connect(), 3); 96 | expect(rhasspyMqttApi.isConnected, false); 97 | verify(mockClient.disconnect()).called(1); 98 | verifyNever(mockClient.subscribe(any, any)); 99 | reset(mockClient); 100 | }); 101 | }); 102 | } 103 | -------------------------------------------------------------------------------- /test/utils_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | import 'dart:typed_data'; 3 | 4 | import 'package:flutter_test/flutter_test.dart'; 5 | import 'package:rhasspy_mobile_app/utils/utils.dart'; 6 | 7 | main() { 8 | test("test wave header", () { 9 | List inputData = []; 10 | inputData 11 | .addAll(File('test_resources/audio/inputAudio1').readAsBytesSync()); 12 | int sampleRate = 16000; 13 | int byteRate = (sampleRate * 16 * 1 ~/ 8); 14 | Uint8List header = waveHeader(inputData.length, sampleRate, 1, byteRate); 15 | inputData.insertAll(0, header); 16 | expect( 17 | inputData, 18 | File('test_resources/audio/outputAudio1.wav').readAsBytesSync(), 19 | ); 20 | }); 21 | } 22 | -------------------------------------------------------------------------------- /test/widget_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_test/flutter_test.dart'; 3 | 4 | import 'package:rhasspy_mobile_app/main.dart'; 5 | 6 | void main() {} 7 | -------------------------------------------------------------------------------- /test_resources/audio/inputAudio1: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/razzo04/rhasspy-mobile-app/3c59971270eab0278cd5dbf6adac4064b5f14908/test_resources/audio/inputAudio1 -------------------------------------------------------------------------------- /test_resources/audio/outputAudio1.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/razzo04/rhasspy-mobile-app/3c59971270eab0278cd5dbf6adac4064b5f14908/test_resources/audio/outputAudio1.wav --------------------------------------------------------------------------------