Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
14 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions mobile_app/.fvmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"flutter": "3.38.3"
}
3 changes: 3 additions & 0 deletions mobile_app/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -190,3 +190,6 @@ Thumbs.db
# Keep important files
!**/lib/l10n/app_*.arb
!**/lib/l10n/app_localizations.dart

# FVM Version Cache
.fvm/
117 changes: 106 additions & 11 deletions mobile_app/lib/bloc/voice_bloc.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../models/models.dart' as models;
import '../services/voice_repository.dart';
Expand Down Expand Up @@ -57,13 +58,21 @@ class ShowBeneficiariesDialog extends VoiceState {
ShowBeneficiariesDialog(this.message, this.beneficiaries, this.sessionId);
}

/// Emitted when the user recorded but said nothing (empty/silent audio).
class RecordingEmpty extends VoiceState {}

sealed class VoiceEvent {}

class StartListening extends VoiceEvent {}
class StartListening extends VoiceEvent {
final String locale;
final String sessionId;
StartListening({required this.locale, required this.sessionId});
}

class StopListening extends VoiceEvent {
final String locale;
StopListening({required this.locale});
final String sessionId;
StopListening({required this.locale, required this.sessionId});
}

class GotTranscript extends VoiceEvent {
Expand All @@ -74,38 +83,110 @@ class GotTranscript extends VoiceEvent {

class Reset extends VoiceEvent {}

/// Fired when user said nothing for initial silence duration (e.g. first 5s).
class InitialSilence extends VoiceEvent {
final String locale;
InitialSilence({required this.locale});
}

class VerifyOtp extends VoiceEvent {
final String otp;
final String sessionId;
final String locale;
VerifyOtp({required this.otp, required this.sessionId, required this.locale});
}

/// User confirmed they want to stop voice banking; stop recording, TTS, timers and reset to Idle.
class CancelVoiceSession extends VoiceEvent {}

class VoiceBloc extends Bloc<VoiceEvent, VoiceState> {
final VoiceRepository repo;
final BankingAPI bank = BankingAPI();
final TTSService tts = TTSService();
String _currentLocale = 'en'; // Track current locale
/// BuildContext of the currently shown dialog (excluding logout). Cleared when dialog is closed or when user speaks and we pop it.
BuildContext? currentDialogContext;
/// Set when user cancels; skip any further TTS and reset to Idle.
bool _voiceSessionCancelled = false;

VoiceBloc(this.repo) : super(Idle()) {
on<StartListening>((e, emit) async {
_voiceSessionCancelled = false;
try {
await repo.start();
await repo.start(
onIdle: () => add(StopListening(locale: e.locale, sessionId: e.sessionId)),
idleDuration: const Duration(seconds: 2),
onInitialSilence: () => add(InitialSilence(locale: e.locale)),
initialSilenceDuration: const Duration(seconds: 12),
onSilenceReminder: () async {
if (_voiceSessionCancelled) return;
final message = TranslationService.translateResponse(
'empty_recording', e.locale, null);
if (_voiceSessionCancelled) return;
try {
await tts.speak(message, langCode: e.locale);
} catch (e) {
print("TTS Error: $e");
}
if (_voiceSessionCancelled) return;
},
silenceReminderDuration: const Duration(seconds: 5),
);
emit(Listening());
} catch (e) {
print("Voice Bloc Error - Permission denied: $e");
// Show a snackbar or dialog to inform user about permission
emit(Idle());
}
});

on<InitialSilence>((e, emit) async {
try {
await repo.stopWithoutTranscribe();
} catch (err) {
print("Voice Bloc Error - stopWithoutTranscribe: $err");
}
emit(Idle());
});

on<CancelVoiceSession>((e, emit) async {
_voiceSessionCancelled = true;
try {
await repo.stopWithoutTranscribe();
} catch (err) {
print("Voice Bloc Error - stopWithoutTranscribe on cancel: $err");
}
try {
await tts.stop();
} catch (err) {
print("TTS stop on cancel: $err");
}
emit(Idle());
});

on<StopListening>((e, emit) async {
emit(Transcribing());
_currentLocale = e.locale; // Store current locale

print("Voice Bloc Debug - Processing normal voice input");
final data = await repo.stopAndTranscribe(locale: e.locale);
add(GotTranscript(data, e.locale));
try {
final data = await repo.stopAndTranscribe(
locale: e.locale,
sessionId: e.sessionId,
ifNotEmptyCallback: () {
if (currentDialogContext != null && currentDialogContext!.mounted) {
Navigator.of(currentDialogContext!).pop();
currentDialogContext = null;
}
},
);
add(GotTranscript(data, e.locale));
} on EmptyRecordingException catch (_) {
emit(RecordingEmpty());
add(Reset());
} catch (err) {
print("Voice Bloc Error - Stop/transcribe failed: $err");
emit(Idle());
}
});

on<Reset>((e, emit) {
Expand Down Expand Up @@ -211,16 +292,18 @@ class VoiceBloc extends Bloc<VoiceEvent, VoiceState> {
originalMessage, _currentLocale, context);

// Speak the translated message
if (_voiceSessionCancelled) return;
try {
await tts.speak(translatedMessage, langCode: ttsLanguage);
} catch (e) {
print("TTS Error: $e");
}
if (_voiceSessionCancelled) return;

// Show transactions dialog
emit(
ShowTransactionsDialog(translatedMessage, transactions, sessionId));
add(Reset());
add(StartListening(locale: _currentLocale, sessionId: sessionId));
return;
}

Expand Down Expand Up @@ -267,17 +350,19 @@ class VoiceBloc extends Bloc<VoiceEvent, VoiceState> {
print("Voice Bloc Debug - Translated message: $translatedMessage");

// Speak the translated message
if (_voiceSessionCancelled) return;
try {
await tts.speak(translatedMessage, langCode: ttsLanguage);
} catch (e) {
print("TTS Error: $e");
}
if (_voiceSessionCancelled) return;

// Show beneficiaries dialog
print("Voice Bloc Debug - Emitting ShowBeneficiariesDialog");
emit(ShowBeneficiariesDialog(
translatedMessage, beneficiaries, sessionId));
add(Reset());
add(StartListening(locale: _currentLocale, sessionId: sessionId));
return;
}

Expand All @@ -304,11 +389,13 @@ class VoiceBloc extends Bloc<VoiceEvent, VoiceState> {
{'amount': amount.toString(), 'recipient': recipient});

// Speak the translated message
if (_voiceSessionCancelled) return;
try {
await tts.speak(translatedMessage, langCode: ttsLanguage);
} catch (e) {
print("TTS Error: $e");
}
if (_voiceSessionCancelled) return;

// Show OTP dialog
emit(ShowOtpDialog(translatedMessage, sessionId, recipient, amount));
Expand Down Expand Up @@ -337,16 +424,18 @@ class VoiceBloc extends Bloc<VoiceEvent, VoiceState> {
message, _currentLocale, {});

// Speak the translated message
if (_voiceSessionCancelled) return;
try {
await tts.speak(translatedMessage, langCode: ttsLanguage);
} catch (e) {
print("TTS Error: $e");
}
if (_voiceSessionCancelled) return;

// Show duplicate beneficiaries dialog
emit(ShowDuplicateDialog(translatedMessage, sessionId, beneficiaries,
originalAmount: originalAmount));
add(Reset());
add(StartListening(locale: _currentLocale, sessionId: sessionId));
return;
}
}
Expand Down Expand Up @@ -386,16 +475,19 @@ class VoiceBloc extends Bloc<VoiceEvent, VoiceState> {
print("Voice Bloc Debug - Translated message: $translatedMessage");

// Speak the translated message
if (_voiceSessionCancelled) return;
try {
await tts.speak(translatedMessage, langCode: ttsLanguage);
} catch (e) {
print("TTS Error: $e");
}
if (_voiceSessionCancelled) return;

// Show the error message
emit(Executing(
translatedMessage, VoiceIntent(VoiceIntentType.unknown)));
add(Reset());
// After TTS completes, restart listening for continuous conversation
add(StartListening(locale: _currentLocale, sessionId: sessionId));
return;
} else {
print(
Expand Down Expand Up @@ -501,11 +593,13 @@ class VoiceBloc extends Bloc<VoiceEvent, VoiceState> {
String translatedMessage = TranslationService.translateApiResponse(
originalMessage, _currentLocale, context);

if (_voiceSessionCancelled) return;
try {
await tts.speak(translatedMessage, langCode: ttsLanguage);
} catch (e) {
print("TTS Error: $e");
}
if (_voiceSessionCancelled) return;

// Determine intent type for execution state
VoiceIntentType intentType = VoiceIntentType.unknown;
Expand All @@ -524,7 +618,8 @@ class VoiceBloc extends Bloc<VoiceEvent, VoiceState> {
}

emit(Executing(translatedMessage, VoiceIntent(intentType)));
add(Reset());
// After TTS completes, restart listening for continuous conversation
add(StartListening(locale: _currentLocale, sessionId: sessionId));
return;
}

Expand Down
12 changes: 11 additions & 1 deletion mobile_app/lib/l10n/app_bn.arb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"loginPrompt": "আপনার মোবাইল নম্বর লিখুন",
"otpPrompt": "ওটিপি লিখুন",
"micHint": "কথা বলার জন্য মাইক্রোফোনে ট্যাপ করুন",
"tapToSpeak": "বলতে ট্যাপ করুন",
"listening": "শোনা হচ্ছে...",
"transcribing": "লেখায় রূপান্তর হচ্ছে...",
"executing": "আপনার অনুরোধ প্রক্রিয়া করা হচ্ছে...",
Expand All @@ -22,6 +23,7 @@
"voice": "ভয়েস",
"stop": "বন্ধ করুন",
"cancel": "বাতিল",
"stopVoiceBankingConfirm": "ভয়েস ব্যাংকিং বন্ধ করবেন? সমস্ত বর্তমান ভয়েস কার্যকলাপ বাতিল করা হবে।",
"welcomeTo": "স্বাগতম",
"experienceBanking": "ভয়েসের শক্তিতে ব্যাংকিংয়ের অভিজ্ঞতা নিন",
"enterMobileNumber": "মোবাইল নম্বর লিখুন",
Expand Down Expand Up @@ -87,5 +89,13 @@
"tipNaturalLanguage": "প্রাকৃতিক ভাষা ব্যবহার করুন যেমন \"আমার ব্যালেন্স দেখান\"",
"tipWaitForIndicator": "কথা বলার আগে শোনার নির্দেশকের জন্য অপেক্ষা করুন",
"needMoreHelp": "আরো সাহায্য প্রয়োজন?",
"contactSupportDescription": "অতিরিক্ত সহায়তার জন্য আমাদের সহায়তা দলকে যোগাযোগ করুন"
"contactSupportDescription": "অতিরিক্ত সহায়তার জন্য আমাদের সহায়তা দলকে যোগাযোগ করুন",
"balanceSuccess": "আপনার বর্তমান জের {amount} টাকা।",
"transactionsFound": "এখানে আপনার {count}টি সাম্প্রতিক লেনদেন।",
"noTransactions": "কোন লেনদেন পাওয়া যায়নি।",
"transferSuccess": "{recipient}-এ {amount} টাকার ট্রান্সফার সফলভাবে শুরু হয়েছে।",
"transferFailed": "ট্রান্সফার ব্যর্থ। অনুগ্রহ করে আবার চেষ্টা করুন।",
"errorGeneric": "দুঃখিত, একটি ত্রুটি হয়েছে। অনুগ্রহ করে আবার চেষ্টা করুন।",
"errorInsufficientFunds": "অপর্যাপ্ত তহবিল। অনুগ্রহ করে আপনার ব্যালেন্স পরীক্ষা করুন।",
"pleaseSaySomething": "অনুগ্রহ করে কিছু বলুন"
}
5 changes: 4 additions & 1 deletion mobile_app/lib/l10n/app_en.arb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"loginPrompt": "Enter your mobile number",
"otpPrompt": "Enter OTP",
"micHint": "Tap the mic to speak",
"tapToSpeak": "Tap to speak",
"listening": "Listening...",
"transcribing": "Transcribing...",
"executing": "Processing your request...",
Expand All @@ -22,6 +23,7 @@
"voice": "Voice",
"stop": "Stop",
"cancel": "Cancel",
"stopVoiceBankingConfirm": "Stop voice banking? All current voice activity will be cancelled.",
"welcomeTo": "Welcome to",
"experienceBanking": "Experience banking with the power of voice",
"enterMobileNumber": "Enter Mobile Number",
Expand Down Expand Up @@ -94,6 +96,7 @@
"transferSuccess": "Transfer of {amount} rupees to {recipient} has been initiated successfully.",
"transferFailed": "Transfer failed. Please try again.",
"errorGeneric": "Sorry, I encountered an error. Please try again.",
"errorInsufficientFunds": "Insufficient funds. Please check your balance."
"errorInsufficientFunds": "Insufficient funds. Please check your balance.",
"pleaseSaySomething": "Please say something"
}

12 changes: 11 additions & 1 deletion mobile_app/lib/l10n/app_gu.arb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"loginPrompt": "તમારો મોબાઇલ નંબર દાખલ કરો",
"otpPrompt": "OTP દાખલ કરો",
"micHint": "બોલવા માટે માઇક પર ટેપ કરો",
"tapToSpeak": "બોલવા ટેપ કરો",
"listening": "સાંભળી રહ્યા છીએ...",
"transcribing": "લખાણમાં રૂપાંતર થઈ રહ્યું છે...",
"executing": "તમારી વિનંતી પ્રક્રિયા કરવામાં આવી રહી છે...",
Expand All @@ -22,6 +23,7 @@
"voice": "વૉઇસ",
"stop": "બંધ કરો",
"cancel": "રદ કરો",
"stopVoiceBankingConfirm": "વૉઇસ બેંકિંગ બંધ કરો? તમામ વર્તમાન વૉઇસ પ્રવૃત્તિ રદ કરવામાં આવશે.",
"welcomeTo": "સ્વાગત છે",
"experienceBanking": "વૉઇસની શક્તિથી બેંકિંગનો અનુભવ કરો",
"enterMobileNumber": "મોબાઇલ નંબર દાખલ કરો",
Expand Down Expand Up @@ -87,5 +89,13 @@
"tipNaturalLanguage": "કુદરતી ભાષા વાપરો જેમ કે \"મારું બેલન્સ બતાવો\"",
"tipWaitForIndicator": "બોલતા પહેલા સાંભળવાના સૂચકની રાહ જુઓ",
"needMoreHelp": "વધુ મદદ જોઈએ?",
"contactSupportDescription": "વધારાની સહાયતા માટે અમારી સહાયતા ટીમનો સંપર્ક કરો"
"contactSupportDescription": "વધારાની સહાયતા માટે અમારી સહાયતા ટીમનો સંપર્ક કરો",
"balanceSuccess": "તમારું વર્તમાન બેલન્સ {amount} રૂપિયા છે.",
"transactionsFound": "અહીં તમારા {count} તાજેતરના લેનદેન છે.",
"noTransactions": "કોઈ લેનદેન મળ્યા નથી.",
"transferSuccess": "{recipient}ને {amount} રૂપિયાનું ટ્રાન્સફર સફળતાપૂર્વક શરૂ થયું છે.",
"transferFailed": "ટ્રાન્સફર નિષ્ફળ. કૃપા કરીને ફરી પ્રયાસ કરો.",
"errorGeneric": "માફ કરો, ભૂલ આવી. કૃપા કરીને ફરી પ્રયાસ કરો.",
"errorInsufficientFunds": "અપૂર્ણ નિધિ. કૃપા કરીને તમારું બેલન્સ તપાસો.",
"pleaseSaySomething": "કૃપા કરીને કંઈક કહો"
}
12 changes: 11 additions & 1 deletion mobile_app/lib/l10n/app_hi.arb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"loginPrompt": "अपना मोबाइल नंबर दर्ज करें",
"otpPrompt": "ओटीपी दर्ज करें",
"micHint": "बोलने के लिए माइक दबाएँ",
"tapToSpeak": "बोलने के लिए टैप करें",
"listening": "सुन रहा हूँ...",
"transcribing": "लिख रहा हूँ...",
"executing": "आपका अनुरोध संसाधित हो रहा है...",
Expand All @@ -22,6 +23,7 @@
"voice": "वॉइस",
"stop": "रोकें",
"cancel": "रद्द करें",
"stopVoiceBankingConfirm": "वॉइस बैंकिंग बंद करें? सभी वर्तमान वॉइस गतिविधि रद्द हो जाएगी।",
"welcomeTo": "स्वागत है",
"experienceBanking": "वॉइस की शक्ति से बैंकिंग का अनुभव करें",
"enterMobileNumber": "मोबाइल नंबर दर्ज करें",
Expand Down Expand Up @@ -87,5 +89,13 @@
"tipNaturalLanguage": "प्राकृतिक भाषा का उपयोग करें जैसे \"मेरा बैलेंस दिखाएं\"",
"tipWaitForIndicator": "बोलने से पहले सुनने के संकेतक की प्रतीक्षा करें",
"needMoreHelp": "और सहायता चाहिए?",
"contactSupportDescription": "अतिरिक्त सहायता के लिए हमारी सहायता टीम से संपर्क करें"
"contactSupportDescription": "अतिरिक्त सहायता के लिए हमारी सहायता टीम से संपर्क करें",
"balanceSuccess": "आपका वर्तमान बैलेंस {amount} रुपये है।",
"transactionsFound": "यहां आपके {count} सबसे हाल के लेनदेन हैं।",
"noTransactions": "कोई लेनदेन नहीं मिला।",
"transferSuccess": "{recipient} को {amount} रुपये का ट्रांसफर सफलतापूर्वक शुरू किया गया है।",
"transferFailed": "ट्रांसफर विफल। कृपया पुनः प्रयास करें।",
"errorGeneric": "क्षमा करें, एक त्रुटि हुई। कृपया पुनः प्रयास करें।",
"errorInsufficientFunds": "अपर्याप्त धन। कृपया अपना बैलेंस जांचें।",
"pleaseSaySomething": "कृपया कुछ बोलें"
}
Loading