Skip to content

Commit

Permalink
Add iOS language setting, resolves #17
Browse files Browse the repository at this point in the history
  • Loading branch information
maxkrieger committed Apr 8, 2022
1 parent 54be65e commit f9db852
Show file tree
Hide file tree
Showing 11 changed files with 222 additions and 75 deletions.
125 changes: 69 additions & 56 deletions ios/Runner/AppDelegate.swift
Original file line number Diff line number Diff line change
@@ -1,71 +1,84 @@
import UIKit
import Flutter
import Speech
import UIKit

@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {

private func receiveTxRequest(call: FlutterMethodCall, result: @escaping FlutterResult) {
let uri = (call.arguments as! [String:Any])["path"] as! String
let url = URL(fileURLWithPath: uri)
guard let myRecognizer = SFSpeechRecognizer() else {
// A recognizer is not supported for the current locale
result(FlutterError(code: "FAILED_REC", message: "unsupported locale", details: nil))
return
}

if !myRecognizer.isAvailable {
result(FlutterError(code: "FAILED_REC", message: "unavailable recognizer", details: nil))
return
}
let uri = (call.arguments as! [String: Any])["path"] as! String
let localeId = (call.arguments as! [String: Any])["locale"] as! String
let url = URL(fileURLWithPath: uri)
guard let myRecognizer = SFSpeechRecognizer(locale: Locale.init(identifier: localeId)) else {
// A recognizer is not supported for the current locale
result(FlutterError(code: "FAILED_REC", message: "unsupported locale", details: nil))
return
}

if !myRecognizer.isAvailable {
result(FlutterError(code: "FAILED_REC", message: "unavailable recognizer", details: nil))
return
}

let request = SFSpeechURLRecognitionRequest(url: url)
myRecognizer.recognitionTask(with: request) { (res, error) in
guard let res = res else {
// Recognition failed, so check error for details and handle it
result(FlutterError(code: "FAILED_REC", message: error.debugDescription, details: nil))
return
}
let request = SFSpeechURLRecognitionRequest(url: url)
myRecognizer.recognitionTask(with: request) { (res, error) in
guard let res = res else {
// Recognition failed, so check error for details and handle it
result(FlutterError(code: "FAILED_REC", message: error.debugDescription, details: nil))
return
}

// Print the speech that has been recognized so far
if res.isFinal {
result(String(res.bestTranscription.formattedString))
}
}
// Print the speech that has been recognized so far
if res.isFinal {
result(String(res.bestTranscription.formattedString))
}
}
}
private func requestTxPermission(result: @escaping FlutterResult) {
let status = SFSpeechRecognizer.authorizationStatus()
switch status {
case .notDetermined:
SFSpeechRecognizer.requestAuthorization({ (status) -> Void in
result(Bool(status == SFSpeechRecognizerAuthorizationStatus.authorized))
})
case .denied:
result(Bool(false))
case .restricted:
result(Bool(false))
case .authorized:
result(Bool(true))
default:
result(Bool(true))
}
}
private func requestTxPermission(result: @escaping FlutterResult) {
let status = SFSpeechRecognizer.authorizationStatus()
switch status {
case .notDetermined:
SFSpeechRecognizer.requestAuthorization({(status)->Void in
result(Bool(status == SFSpeechRecognizerAuthorizationStatus.authorized))
})
case .denied:
result(Bool(false))
case .restricted:
result(Bool(false))
case .authorized:
result(Bool(true))
default:
result(Bool(true))
}
private func getLocaleOptions(result: @escaping FlutterResult) {
let locales = SFSpeechRecognizer.supportedLocales()
var localeOptions: [String: String] = [:]
for locale in locales {
localeOptions[locale.identifier] = locale.localizedString(forIdentifier: locale.identifier)!
}
result(localeOptions)
}
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
let controller : FlutterViewController = window?.rootViewController as! FlutterViewController
let transcribeChannel = FlutterMethodChannel(name: "voiceoutliner.saga.chat/iostx", binaryMessenger: controller.binaryMessenger)
transcribeChannel.setMethodCallHandler({(call: FlutterMethodCall, result: @escaping FlutterResult)-> Void in
switch call.method {
case "transcribe":
self.receiveTxRequest(call: call, result: result)
case "requestPermission":
self.requestTxPermission(result: result)
default:
result(FlutterMethodNotImplemented)
}
})
GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
let controller: FlutterViewController = window?.rootViewController as! FlutterViewController
let transcribeChannel = FlutterMethodChannel(
name: "voiceoutliner.saga.chat/iostx", binaryMessenger: controller.binaryMessenger)
transcribeChannel.setMethodCallHandler({
(call: FlutterMethodCall, result: @escaping FlutterResult) -> Void in
switch call.method {
case "transcribe":
self.receiveTxRequest(call: call, result: result)
case "requestPermission":
self.requestTxPermission(result: result)
case "getLocaleOptions":
self.getLocaleOptions(result: result)
default:
result(FlutterMethodNotImplemented)
}
})
GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
}
1 change: 1 addition & 0 deletions lib/consts.dart
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ const lastRouteKey = "last_route";
const lastOutlineKey = "last_outline";
const modelDirKey = "model_dir";
const modelLanguageKey = "model_language";
const localeKey = "ios_locale";

const classicPurple = Color.fromRGBO(169, 129, 234, 1.0);
const basePurple = Color.fromRGBO(163, 95, 255, 1);
Expand Down
15 changes: 12 additions & 3 deletions lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,20 @@ import 'package:voice_outliner/repositories/drive_backup.dart';
import 'package:voice_outliner/repositories/vosk_speech_recognizer.dart';
import 'package:voice_outliner/state/outline_state.dart';
import 'package:voice_outliner/state/player_state.dart';
import 'package:voice_outliner/views/ios_transcription_setup_view.dart';
import 'package:voice_outliner/views/notes_view.dart';
import 'package:voice_outliner/views/onboarding_view.dart';
import 'package:voice_outliner/views/outlines_view.dart';
import 'package:voice_outliner/views/transcription_setup_view.dart';
import 'package:voice_outliner/views/vosk_transcription_setup_view.dart';

import 'consts.dart';

final routes = {
"/": const OutlinesView(),
"/notes": const NotesView(),
"/onboarding": const OnboardingView(),
"/transcription_setup": const TranscriptionSetupView()
"/transcription_setup_vosk": const VoskTranscriptionSetupView(),
"/transcription_setup_ios": const IOSTranscriptionSetupView()
};

const generalAppBar =
Expand Down Expand Up @@ -118,7 +120,14 @@ class _VoiceOutlinerAppState extends State<VoiceOutlinerApp> {
widget.sharedPreferences.getString(modelDirKey) == null &&
(widget.sharedPreferences.getBool(shouldTranscribeKey) ?? true) &&
lastRoute != null) {
return "/transcription_setup";
return "/transcription_setup_vosk";
}
// If you've onboarded but don't have language set up on iOS
if (Platform.isIOS &&
widget.sharedPreferences.getString(localeKey) == null &&
(widget.sharedPreferences.getBool(shouldTranscribeKey) ?? true) &&
lastRoute != null) {
return "/transcription_setup_ios";
}
if (lastRoute != null) {
return lastRoute;
Expand Down
21 changes: 18 additions & 3 deletions lib/repositories/ios_speech_recognizer.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@ import 'package:flutter/services.dart';

const iosPlatform = MethodChannel("voiceoutliner.saga.chat/iostx");

Future<String?> recognizeNoteIOS(String path) async {
Future<String?> recognizeNoteIOS(String path, String locale) async {
try {
final platformRes =
await iosPlatform.invokeMethod("transcribe", {"path": path});
final platformRes = await iosPlatform
.invokeMethod("transcribe", {"path": path, "locale": locale});
if (platformRes is String) {
return platformRes;
} else {
Expand All @@ -32,3 +32,18 @@ Future<bool> tryTxPermissionIOS() async {
return false;
}
}

/// Returns a map {"en-US": "English (US)"}
Future<Map<String, String>> getLocaleOptions() async {
if (!Platform.isIOS) {
print("Not IOS");
return {};
}
try {
final res = await iosPlatform.invokeMethod("getLocaleOptions");
return Map<String, String>.from(res);
} catch (err) {
print(err);
return {};
}
}
6 changes: 5 additions & 1 deletion lib/state/notes_state.dart
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ class NotesModel extends ChangeNotifier {
bool shouldLocate = false;
bool showCompleted = true;
bool isReady = false;
String locale = "en-US";
Completer<bool> _readyCompleter = Completer();
bool isIniting = false;
final LinkedList<Note> notes = LinkedList<Note>();
Expand Down Expand Up @@ -78,7 +79,7 @@ class NotesModel extends ChangeNotifier {
if (shouldTranscribe && !note.transcribed && note.filePath != null) {
final path = _playerModel.getPathFromFilename(note.filePath!);
final res = Platform.isIOS
? await recognizeNoteIOS(path)
? await recognizeNoteIOS(path, locale)
: await voskSpeechRecognize(path);
// Guard against writing after user went back
if (isReady) {
Expand Down Expand Up @@ -469,6 +470,9 @@ class NotesModel extends ChangeNotifier {
shouldTranscribe = prefs.getBool(shouldTranscribeKey) ?? false;
shouldLocate = prefs.getBool(shouldLocateKey) ?? false;
showCompleted = prefs.getBool(showCompletedKey) ?? true;
if (Platform.isIOS) {
locale = prefs.getString(localeKey) ?? locale;
}
isReady = true;
_readyCompleter.complete(true);
_readyCompleter = Completer();
Expand Down
32 changes: 32 additions & 0 deletions lib/views/ios_transcription_setup_view.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import 'package:flutter/material.dart';
import 'package:voice_outliner/widgets/ios_locale_selector.dart';

class IOSTranscriptionSetupView extends StatelessWidget {
const IOSTranscriptionSetupView({Key? key}) : super(key: key);

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("Setup"),
automaticallyImplyLeading: false,
),
body: Padding(
padding: const EdgeInsets.all(20.0),
child: Center(
child: Column(children: [
const Text(
"Select transcription language",
style: TextStyle(fontSize: 18.0),
),
const SizedBox(height: 20),
const IOSLocaleSelector(),
const SizedBox(height: 20),
ElevatedButton(
onPressed: () => Navigator.pushNamedAndRemoveUntil(
context, "/", (route) => false),
child: const Text("continue"))
]))),
);
}
}
6 changes: 5 additions & 1 deletion lib/views/onboarding_view.dart
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,11 @@ class _OnboardingViewState extends State<OnboardingView> {

void onDone() {
Navigator.pushNamedAndRemoveUntil(
context, Platform.isIOS ? "/" : "/transcription_setup", (_) => false);
context,
Platform.isIOS
? "/transcription_setup_ios"
: "/transcription_setup_vosk",
(_) => false);
}

@override
Expand Down
22 changes: 18 additions & 4 deletions lib/views/settings_view.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ import 'package:voice_outliner/consts.dart';
import 'package:voice_outliner/repositories/ios_speech_recognizer.dart';
import 'package:voice_outliner/state/outline_state.dart';
import 'package:voice_outliner/views/drive_settings_view.dart';
import 'package:voice_outliner/views/transcription_setup_view.dart';
import 'package:voice_outliner/views/ios_transcription_setup_view.dart';
import 'package:voice_outliner/views/vosk_transcription_setup_view.dart';

class SettingsView extends StatefulWidget {
const SettingsView({Key? key}) : super(key: key);
Expand All @@ -28,7 +29,6 @@ class _SettingsViewState extends State<SettingsView> {

Future<void> init() async {
sharedPreferences = await SharedPreferences.getInstance();

setState(() {
isInited = true;
});
Expand Down Expand Up @@ -114,7 +114,7 @@ class _SettingsViewState extends State<SettingsView> {
? Column(
children: [
const SizedBox(height: 10.0),
if (Platform.isIOS)
if (Platform.isIOS) ...[
SwitchListTile(
secondary: const Icon(Icons.voicemail),
title: const Text("Transcribe Recordings"),
Expand All @@ -136,14 +136,28 @@ class _SettingsViewState extends State<SettingsView> {
sharedPreferences.setBool(shouldTranscribeKey, v);
});
}),
if (sharedPreferences.getBool(shouldTranscribeKey) ?? true)
ListTile(
leading: const Icon(Icons.language),
trailing: const Icon(Icons.arrow_forward_ios),
title: const Text("Transcription Language"),
onTap: () => Navigator.push(
context,
MaterialPageRoute(
builder: (_) =>
const IOSTranscriptionSetupView())),
),
],
if (Platform.isAndroid)
ListTile(
leading: const Icon(Icons.voicemail),
trailing: const Icon(Icons.arrow_forward_ios),
title: const Text("Transcription Setup"),
onTap: () => Navigator.push(
context,
MaterialPageRoute(
builder: (_) => const TranscriptionSetupView())),
builder: (_) =>
const VoskTranscriptionSetupView())),
),
SwitchListTile(
secondary: const Icon(Icons.location_pin),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,16 @@ import 'package:voice_outliner/consts.dart';
import 'package:voice_outliner/repositories/ios_speech_recognizer.dart';
import 'package:voice_outliner/repositories/vosk_speech_recognizer.dart';

class TranscriptionSetupView extends StatefulWidget {
const TranscriptionSetupView({Key? key}) : super(key: key);
class VoskTranscriptionSetupView extends StatefulWidget {
const VoskTranscriptionSetupView({Key? key}) : super(key: key);

@override
_TranscriptionSetupViewState createState() => _TranscriptionSetupViewState();
_VoskTranscriptionSetupViewState createState() =>
_VoskTranscriptionSetupViewState();
}

class _TranscriptionSetupViewState extends State<TranscriptionSetupView> {
class _VoskTranscriptionSetupViewState
extends State<VoskTranscriptionSetupView> {
SharedPreferences? sharedPreferences;
bool isInited = false;
bool loading = false;
Expand Down
Loading

0 comments on commit f9db852

Please sign in to comment.