diff options
author | Anuj Solanki <anuj603362@gmail.com> | 2024-09-29 21:31:03 +0530 |
---|---|---|
committer | Jan-Simon Moeller <jsmoeller@linuxfoundation.org> | 2024-10-07 10:52:41 +0000 |
commit | f870bbe3c49d421ff8ea561752b3b0a38ad04e96 (patch) | |
tree | e6607974a3ea6591f622a3ebe8010561b0a6ad26 /lib/presentation/screens | |
parent | 29ae7d2d9e04bd8e3a7d37dcfa87a02dd1ab385f (diff) |
Integrate voice assistant into flutter-ics-homescreen
- Implement voice-agent client to connect with agl-service-voiceagent
for command execution, wake word detection.
- Add a setting tile on the settings page for configuring voice
assistant settings.
- Add toggle buttons for wake word mode, online mode, overlay and
speech-to-text model in the voice assistant settings.
- Add a button on the homepage to start the voice assistant.
- Update gRPC protos to retrieve online-mode status from the voice
service.
- Make online-mode tile conditional in voice-assistant settings,
removing it from the UI if not enabled in the service.
- Automatically hide the overlay 3 seconds after command execution.
Bug-AGL: SPEC-5200
Change-Id: I4efaaf16ebc570b28816dc7203364efe2b658c2e
Signed-off-by: Anuj Solanki <anuj603362@gmail.com>
Diffstat (limited to 'lib/presentation/screens')
7 files changed, 632 insertions, 0 deletions
diff --git a/lib/presentation/screens/home/home.dart b/lib/presentation/screens/home/home.dart index 0ee52ac..6e3e119 100644 --- a/lib/presentation/screens/home/home.dart +++ b/lib/presentation/screens/home/home.dart @@ -1,4 +1,6 @@ import 'package:flutter_ics_homescreen/export.dart'; + +import '../../common_widget/voice_assistant_button.dart'; // import 'package:media_kit_video/media_kit_video.dart'; final bkgImageProvider = Provider((ref) { @@ -76,6 +78,15 @@ class HomeScreenState extends ConsumerState<HomeScreen> { height: 500, child: const VolumeFanControl()), ), + // Voice Assistant Button + if (appState != AppState.splash && ref.watch(voiceAssistantStateProvider.select((value)=>value.isVoiceAssistantEnable))) + Positioned( + top: MediaQuery.of(context).size.height * 0.82, + child: Container( + padding: const EdgeInsets.only(left: 8), + child: const VoiceAssistantButton() + ), + ), ], ), bottomNavigationBar: diff --git a/lib/presentation/screens/settings/settings_screens/voice_assistant/voice_assistant_screen.dart b/lib/presentation/screens/settings/settings_screens/voice_assistant/voice_assistant_screen.dart new file mode 100644 index 0000000..e1f38ae --- /dev/null +++ b/lib/presentation/screens/settings/settings_screens/voice_assistant/voice_assistant_screen.dart @@ -0,0 +1,28 @@ + +import 'package:flutter_ics_homescreen/export.dart'; +import 'widgets/voice_assistant_content.dart'; + +class VoiceAssistantPage extends ConsumerWidget{ + const VoiceAssistantPage({super.key}); + + static Page<void> page() => const MaterialPage<void>(child: VoiceAssistantPage()); + @override + Widget build(BuildContext context,WidgetRef ref) { + + return Scaffold( + body: Column( + children: [ + CommonTitle( + title: 'Voice Assistant', + hasBackButton: true, + onPressed: () { + ref.read(appProvider.notifier).back(); + }, + ), + Expanded(child: VoiceAssistantContent()), + ], + ), + ); + } +} + diff --git a/lib/presentation/screens/settings/settings_screens/voice_assistant/widgets/stt_model/stt_model_screen.dart b/lib/presentation/screens/settings/settings_screens/voice_assistant/widgets/stt_model/stt_model_screen.dart new file mode 100644 index 0000000..614763d --- /dev/null +++ b/lib/presentation/screens/settings/settings_screens/voice_assistant/widgets/stt_model/stt_model_screen.dart @@ -0,0 +1,120 @@ +import 'package:flutter_ics_homescreen/export.dart'; + +import '../../../../../../../data/models/voice_assistant_state.dart'; + +class STTModelPage extends ConsumerWidget { + const STTModelPage({super.key}); + + static Page<void> page() => + const MaterialPage<void>(child: STTModelPage()); + @override + Widget build(BuildContext context, WidgetRef ref) { + final SttModel sttModel = ref.watch(voiceAssistantStateProvider.select((value) => value.sttModel)); + + return Scaffold( + body: Column( + children: [ + CommonTitle( + title: 'Speech to Text Model', + hasBackButton: true, + onPressed: () { + context.flow<AppState>().update((state) => AppState.voiceAssistant); + }, + ), + Expanded( + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 0, horizontal: 144), + child: ListView( + children: [ + Container( + height: 130, + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.centerLeft, + end: Alignment.centerRight, + stops: sttModel == SttModel.whisper + ? [0, 0.01, 0.8] + : [0.1, 1], + colors: sttModel == SttModel.whisper + ? <Color>[ + Colors.white, + Colors.blue, + const Color.fromARGB(16, 41, 98, 255) + ] + : <Color>[Colors.black, Colors.black12]), + ), + child: ListTile( + minVerticalPadding: 0.0, + contentPadding: const EdgeInsets.symmetric( + horizontal: 16.0, vertical: 40.0), + leading: Text( + 'Whisper AI', + style: Theme.of(context).textTheme.titleMedium, + ), + trailing: sttModel == SttModel.whisper + ? const Icon( + Icons.done, + color: AGLDemoColors.periwinkleColor, + size: 48, + ) + : null, + onTap: () { + ref + .read(voiceAssistantStateProvider.notifier) + .updateSttModel(SttModel.whisper); + }), + ), + const SizedBox( + height: 8, + ), + Container( + height: 130, + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.centerLeft, + end: Alignment.centerRight, + stops: sttModel == SttModel.vosk + ? [0, 0.01, 0.8] + : [0.1, 1], + colors: sttModel == SttModel.vosk + ? <Color>[ + Colors.white, + Colors.blue, + const Color.fromARGB(16, 41, 98, 255) + ] + : <Color>[Colors.black, Colors.black12]), + ), + child: ListTile( + minVerticalPadding: 0.0, + contentPadding: const EdgeInsets.symmetric( + horizontal: 16.0, vertical: 40.0), + leading: Text( + 'Vosk', + style: Theme.of(context).textTheme.titleMedium, + ), + //title: Text(widget.title), + //enabled: isSwitchOn, + trailing: sttModel == SttModel.vosk + ? const Icon( + Icons.done, + color: AGLDemoColors.periwinkleColor, + size: 48, + ) + : null, + + onTap: () { + ref + .read(voiceAssistantStateProvider.notifier) + .updateSttModel(SttModel.vosk); + }, + ), + ), + ], + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/presentation/screens/settings/settings_screens/voice_assistant/widgets/voice_assistant_content.dart b/lib/presentation/screens/settings/settings_screens/voice_assistant/widgets/voice_assistant_content.dart new file mode 100644 index 0000000..924a219 --- /dev/null +++ b/lib/presentation/screens/settings/settings_screens/voice_assistant/widgets/voice_assistant_content.dart @@ -0,0 +1,251 @@ +import 'package:flutter_ics_homescreen/export.dart'; + +import 'package:flutter_ics_homescreen/export.dart'; +import 'package:flutter_ics_homescreen/presentation/screens/settings/settings_screens/voice_assistant/widgets/voice_assistant_tile.dart'; + +import '../../../../../../core/utils/helpers.dart'; +import '../../../../../../data/models/voice_assistant_state.dart'; + +@immutable +class VoiceAssistantContent extends ConsumerWidget { + VoiceAssistantContent({Key? key}) : super(key: key); + bool isWakeWordMode = false; + bool isVoiceAssistantOverlay = false; + bool isOnlineMode = false; + SttModel sttModel = SttModel.whisper; + + @override + Widget build(BuildContext context, WidgetRef ref) { + isWakeWordMode = + ref.watch(voiceAssistantStateProvider.select((value) => value.isWakeWordMode)); + isVoiceAssistantOverlay = + ref.watch(voiceAssistantStateProvider.select((value) => value.voiceAssistantOverlay)); + isOnlineMode = + ref.watch(voiceAssistantStateProvider.select((value) => value.isOnlineMode)); + sttModel = + ref.watch(voiceAssistantStateProvider.select((value) => value.sttModel)); + + final wakeWordCallback = () { + bool status = ref.read(voiceAssistantStateProvider.notifier).toggleWakeWordMode(); + if(status){ + var voiceAgentClient = ref.read(voiceAgentClientProvider); + voiceAgentClient.startWakeWordDetection(); + } + }; + + final voiceAssistantOverlayCallback = () { + ref.read(voiceAssistantStateProvider.notifier).toggleVoiceAssistantOverlay(); + }; + + final onlineModeCallback = () { + ref.read(voiceAssistantStateProvider.notifier).toggleOnlineMode(); + }; + + + return Column( + children: [ + Expanded( + child: ListView( + padding: const EdgeInsets.symmetric(vertical: 50, horizontal: 144), + children: [ + VoiceAssistantTile( + icon: Icons.insert_comment_outlined, + title: "Voice Assistant Overlay", + hasSwitch: true, + voidCallback: voiceAssistantOverlayCallback, + isSwitchOn: isVoiceAssistantOverlay + ), + if(ref.watch(voiceAssistantStateProvider.select((value) => value.isOnlineModeAvailable))) + VoiceAssistantTile( + icon: Icons.cloud_circle, + title: "Online Mode", + hasSwitch: true, + voidCallback: onlineModeCallback, + isSwitchOn: isOnlineMode + ), + VoiceAssistantTile( + icon: Icons.mic_none_outlined, + title: "Wake Word Mode", + hasSwitch: true, + voidCallback: wakeWordCallback, + isSwitchOn: isWakeWordMode + ), + if(ref.watch(voiceAssistantStateProvider.select((value) => value.isWakeWordMode))) + WakeWordTile(), + SttTile( + title: " Speech To Text", + sttName: sttModel==SttModel.whisper ? "Whisper AI" : "Vosk", + hasSwich: true, + voidCallback: () async { + context + .flow<AppState>() + .update((next) => AppState.sttModel); + }), + ], + ) + ), + ], + ); + } +} + +class SttTile extends ConsumerStatefulWidget { + final IconData? icon; + final String title; + final String sttName; + final bool hasSwich; + final VoidCallback voidCallback; + final String? image; + const SttTile({ + Key? key, + this.icon, + required this.title, + required this.sttName, + required this.hasSwich, + required this.voidCallback, + this.image, + }) : super(key: key); + + @override + SttTileState createState() => SttTileState(); +} + +class SttTileState extends ConsumerState<SttTile> { + @override + Widget build(BuildContext context) { + return Column( + children: [ + Container( + margin: const EdgeInsets.symmetric(vertical: 8), + padding: const EdgeInsets.symmetric(vertical: 15), + decoration: const BoxDecoration( + gradient: LinearGradient( + begin: Alignment.centerLeft, + end: Alignment.centerRight, + stops: [0.3, 1], + colors: <Color>[Colors.black, Colors.black12]), + ), + //color: Color(0xFF0D113F), + child: ListTile( + contentPadding: + const EdgeInsets.symmetric(vertical: 17, horizontal: 24), + leading: Icon( + Icons.transcribe_outlined, + color: AGLDemoColors.periwinkleColor, + size: 48, + ), + title: Text( + widget.title, + style: TextStyle( + color: AGLDemoColors.periwinkleColor, + shadows: [ + Helpers.dropShadowRegular, + ], + fontSize: 40), + ), + trailing: Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Text( + widget.sttName, + style: TextStyle( + color: AGLDemoColors.periwinkleColor, + shadows: [ + Helpers.dropShadowRegular, + ], + fontSize: 40, + ), + ), + const SizedBox( + width: 24, + ), + const Icon( + Icons.arrow_forward_ios, + color: AGLDemoColors.periwinkleColor, + size: 48, + ), + ], + ), + onTap: widget.voidCallback, + ), + ), + const SizedBox( + height: 8, + ) + ], + ); + } +} + + + +class WakeWordTile extends ConsumerStatefulWidget { + const WakeWordTile({Key? key}) : super(key: key); + + @override + WakeWordTileState createState() => WakeWordTileState(); +} + +class WakeWordTileState extends ConsumerState<WakeWordTile> { + @override + Widget build(BuildContext context) { + return Column( + children: [ + Container( + margin: const EdgeInsets.symmetric(vertical: 8), + padding: const EdgeInsets.symmetric(vertical: 15), + decoration: const BoxDecoration( + gradient: LinearGradient( + begin: Alignment.centerLeft, + end: Alignment.centerRight, + stops: [0.3, 1], + colors: <Color>[Colors.black, Colors.black12]), + ), + //color: Color(0xFF0D113F), + child: ListTile( + contentPadding: + const EdgeInsets.symmetric(vertical: 17, horizontal: 24), + leading: Icon( + Icons.mic_none_outlined, + color: AGLDemoColors.periwinkleColor, + size: 48, + ), + title: Text( + "Wake Word", + style: TextStyle( + color: AGLDemoColors.periwinkleColor, + shadows: [ + Helpers.dropShadowRegular, + ], + fontSize: 40), + ), + trailing: Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Text( + ref.watch(voiceAssistantStateProvider.select((value) => value.wakeWord)) ?? "Not Set", + style: TextStyle( + color: AGLDemoColors.periwinkleColor, + shadows: [ + Helpers.dropShadowRegular, + ], + fontSize: 40, + ), + ), + const SizedBox( + width: 50, + ), + + ], + ), + ), + ), + const SizedBox( + height: 8, + ) + ], + ); + } +} diff --git a/lib/presentation/screens/settings/settings_screens/voice_assistant/widgets/voice_assistant_settings_list_tile.dart b/lib/presentation/screens/settings/settings_screens/voice_assistant/widgets/voice_assistant_settings_list_tile.dart new file mode 100644 index 0000000..ee0365a --- /dev/null +++ b/lib/presentation/screens/settings/settings_screens/voice_assistant/widgets/voice_assistant_settings_list_tile.dart @@ -0,0 +1,111 @@ +import 'package:flutter_ics_homescreen/export.dart'; +import 'package:protos/val_api.dart'; + +class VoiceAssistantSettingsTile extends ConsumerStatefulWidget { + final IconData icon; + final String title; + final bool hasSwich; + final VoidCallback voidCallback; + const VoiceAssistantSettingsTile({ + Key? key, + required this.icon, + required this.title, + required this.hasSwich, + required this.voidCallback, + }) : super(key: key); + + @override + VoiceAssistantSettingsTileState createState() => VoiceAssistantSettingsTileState(); +} + +class VoiceAssistantSettingsTileState extends ConsumerState<VoiceAssistantSettingsTile> { + bool isSwitchOn = true; + @override + Widget build(BuildContext context) { + isSwitchOn = ref.watch(voiceAssistantStateProvider.select((voiceAssistant) => voiceAssistant.isVoiceAssistantEnable)); + return Column( + children: [ + GestureDetector( + onTap: isSwitchOn ? widget.voidCallback : () {}, + child: Container( + height: 130, + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.centerLeft, + end: Alignment.centerRight, + stops: isSwitchOn ? [0.3, 1] : [0.8, 1], + colors: isSwitchOn + ? <Color>[Colors.black, Colors.black12] + : <Color>[ + const Color.fromARGB(50, 0, 0, 0), + Colors.transparent + ], + ), + ), + child: Card( + color: Colors.transparent, + elevation: 5, + child: Padding( + padding: + const EdgeInsets.symmetric(vertical: 0, horizontal: 24), + child: Row( + children: [ + Icon( + widget.icon, + color: AGLDemoColors.periwinkleColor, + size: 48, + ), + const SizedBox(width: 24), + Expanded( + child: Text( + widget.title, + style: const TextStyle(fontSize: 40), + ), + ), + widget.hasSwich + ? Container( + width: 126, + height: 80, + decoration: const ShapeDecoration( + color: + AGLDemoColors.gradientBackgroundDarkColor, + shape: StadiumBorder( + side: BorderSide( + color: Color(0xFF5477D4), + width: 4, + )), + ), + child: FittedBox( + fit: BoxFit.fill, + child: Switch( + value: isSwitchOn, + onChanged: (bool value) async { + var voiceAgentClient = ref.read(voiceAgentClientProvider); + ServiceStatus status = await voiceAgentClient.checkServiceStatus(); + ref.read(voiceAssistantStateProvider.notifier).toggleVoiceAssistant(status); + setState(() { + isSwitchOn = value; + }); + // This is called when the user toggles the switch. + }, + inactiveTrackColor: Colors.transparent, + activeTrackColor: Colors.transparent, + thumbColor: + MaterialStateProperty.all<Color>( + AGLDemoColors.periwinkleColor)), + ), + ) + : const SizedBox(), + ], + ), + ), + ) + ), + ), + const SizedBox( + height: 8, + ) + ], + ); + } +} diff --git a/lib/presentation/screens/settings/settings_screens/voice_assistant/widgets/voice_assistant_tile.dart b/lib/presentation/screens/settings/settings_screens/voice_assistant/widgets/voice_assistant_tile.dart new file mode 100644 index 0000000..d4bdd48 --- /dev/null +++ b/lib/presentation/screens/settings/settings_screens/voice_assistant/widgets/voice_assistant_tile.dart @@ -0,0 +1,102 @@ +import 'package:flutter_ics_homescreen/export.dart'; + +class VoiceAssistantTile extends ConsumerStatefulWidget { + final IconData icon; + final String title; + final bool hasSwitch; + final VoidCallback voidCallback; + final bool isSwitchOn; + const VoiceAssistantTile({super.key, required this.icon, required this.title, required this.hasSwitch, required this.voidCallback,required this.isSwitchOn}); + + @override + ConsumerState<VoiceAssistantTile> createState() => _VoiceAssistantTileState(); +} + +class _VoiceAssistantTileState extends ConsumerState<VoiceAssistantTile> { + bool isSwitchOn = true; + @override + Widget build(BuildContext context) { + isSwitchOn = widget.isSwitchOn; + return Column( + children: [ + Container( + height: 130, + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.centerLeft, + end: Alignment.centerRight, + stops: isSwitchOn ? [0.3, 1] : [0.8, 1], + colors: isSwitchOn + ? <Color>[Colors.black, Colors.black12] + : <Color>[ + const Color.fromARGB(50, 0, 0, 0), + Colors.transparent + ], + ), + ), + child: Card( + + color: Colors.transparent, + elevation: 5, + child: Padding( + padding: + const EdgeInsets.symmetric(vertical: 0, horizontal: 24), + child: Row( + children: [ + Icon( + widget.icon, + color: AGLDemoColors.periwinkleColor, + size: 48, + ), + const SizedBox(width: 24), + Expanded( + child: Text( + widget.title, + style: const TextStyle(fontSize: 40), + ), + ), + widget.hasSwitch + ? Container( + width: 126, + height: 80, + decoration: const ShapeDecoration( + color: + AGLDemoColors.gradientBackgroundDarkColor, + shape: StadiumBorder( + side: BorderSide( + color: Color(0xFF5477D4), + width: 4, + )), + ), + child: FittedBox( + fit: BoxFit.fill, + child: Switch( + value: isSwitchOn, + onChanged: (bool value) { + setState(() { + isSwitchOn = value; + }); + widget.voidCallback(); + }, + inactiveTrackColor: Colors.transparent, + activeTrackColor: Colors.transparent, + thumbColor: + MaterialStateProperty.all<Color>( + AGLDemoColors.periwinkleColor)), + ), + ) + : const SizedBox(), + ], + ), + ), + ) + ), + const SizedBox( + height: 14, + ) + ], + ); + } +} + + diff --git a/lib/presentation/screens/settings/widgets/settings_content.dart b/lib/presentation/screens/settings/widgets/settings_content.dart index 6d0df50..458677c 100644 --- a/lib/presentation/screens/settings/widgets/settings_content.dart +++ b/lib/presentation/screens/settings/widgets/settings_content.dart @@ -1,6 +1,7 @@ import 'package:flutter_ics_homescreen/export.dart'; import '../../../custom_icons/custom_icons.dart'; +import '../settings_screens/voice_assistant/widgets/voice_assistant_settings_list_tile.dart'; class Settings extends ConsumerWidget { const Settings({ @@ -55,6 +56,14 @@ class Settings extends ConsumerWidget { voidCallback: () { ref.read(appProvider.notifier).update(AppState.audioSettings); }), + VoiceAssistantSettingsTile( + icon: Icons.keyboard_voice_outlined, + title: "Voice Assistant", + hasSwich: true, + voidCallback: (){ + ref.read(appProvider.notifier).update(AppState.voiceAssistant); + } + ), SettingsTile( icon: Icons.person_2_outlined, title: 'Profiles', |