diff --git a/app/lib/pages/onboarding/speech_profile_widget.dart b/app/lib/pages/onboarding/speech_profile_widget.dart index 8670b8381..b34b5616f 100644 --- a/app/lib/pages/onboarding/speech_profile_widget.dart +++ b/app/lib/pages/onboarding/speech_profile_widget.dart @@ -3,6 +3,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_provider_utilities/flutter_provider_utilities.dart'; import 'package:friend_private/backend/preferences.dart'; +import 'package:friend_private/pages/speech_profile/percentage_bar_progress.dart'; import 'package:friend_private/providers/capture_provider.dart'; import 'package:friend_private/providers/speech_profile_provider.dart'; import 'package:friend_private/widgets/dialog.dart'; @@ -138,6 +139,23 @@ class _SpeechProfileWidgetState extends State with TickerPr ), barrierDismissible: false, ); + } else if (error == "NO_SPEECH") { + showDialog( + context: context, + builder: (c) => getDialog( + context, + () { + Navigator.pop(context); + // Navigator.pop(context); + }, + () {}, + 'Are you there?', + 'We could not detect any speech. Please make sure to speak for at least 10 seconds and not more than 3 minutes.', + okButtonText: 'Ok', + singleButton: true, + ), + barrierDismissible: false, + ); } }, child: Container( @@ -148,17 +166,23 @@ class _SpeechProfileWidgetState extends State with TickerPr height: 10, ), Padding( - padding: const EdgeInsets.fromLTRB(40, 20, 40, 20), + padding: EdgeInsets.fromLTRB(40, !provider.startedRecording ? 20 : 0, 40, 20), child: !provider.startedRecording - ? const Text( - 'Now, Omi needs to learn your voice to be able to recognise you.', - textAlign: TextAlign.center, - style: TextStyle( - color: Colors.white, - fontSize: 20, - height: 1.4, - fontWeight: FontWeight.w400, - ), + ? const Column( + children: [ + Text( + 'Omi needs to learn your voice to recognize you', + textAlign: TextAlign.center, + style: TextStyle( + color: Colors.white, + fontSize: 20, + height: 1.4, + fontWeight: FontWeight.w400, + ), + ), + SizedBox(height: 14), + Text("Note: This only works in English", style: TextStyle(color: Colors.white)), + ], ) : LayoutBuilder( builder: (context, constraints) { @@ -200,128 +224,118 @@ class _SpeechProfileWidgetState extends State with TickerPr }, ), ), - Padding( - padding: const EdgeInsets.fromLTRB(24, 0, 24, 0), - child: !provider.startedRecording - ? (provider.isInitialising - ? const CircularProgressIndicator( - color: Colors.white, - ) - : Column( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - const SizedBox(height: 20), - Container( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 0), - decoration: BoxDecoration( - border: const GradientBoxBorder( - gradient: LinearGradient(colors: [ - Color.fromARGB(127, 208, 208, 208), - Color.fromARGB(127, 188, 99, 121), - Color.fromARGB(127, 86, 101, 182), - Color.fromARGB(127, 126, 190, 236) - ]), - width: 2, - ), - borderRadius: BorderRadius.circular(12), - ), - child: TextButton( - onPressed: () async { - await stopDeviceRecording(); - await provider.initialise(finalizedCallback: restartDeviceRecording); - provider.forceCompletionTimer = - Timer(Duration(seconds: provider.maxDuration), () async { - provider.finalize(); - }); - provider.updateStartedRecording(true); - }, - child: const Text( - 'Get Started', - style: TextStyle(color: Colors.white, fontSize: 16), - ), + !provider.startedRecording + ? (provider.isInitialising + ? const CircularProgressIndicator( + color: Colors.white, + ) + : Column( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + const SizedBox(height: 20), + Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 0), + decoration: BoxDecoration( + border: const GradientBoxBorder( + gradient: LinearGradient(colors: [ + Color.fromARGB(127, 208, 208, 208), + Color.fromARGB(127, 188, 99, 121), + Color.fromARGB(127, 86, 101, 182), + Color.fromARGB(127, 126, 190, 236) + ]), + width: 2, ), + borderRadius: BorderRadius.circular(12), ), - const SizedBox(height: 10), - ], - )) - : provider.profileCompleted - ? Container( - margin: const EdgeInsets.only(top: 40), - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 0), - decoration: BoxDecoration( - border: const GradientBoxBorder( - gradient: LinearGradient(colors: [ - Color.fromARGB(127, 208, 208, 208), - Color.fromARGB(127, 188, 99, 121), - Color.fromARGB(127, 86, 101, 182), - Color.fromARGB(127, 126, 190, 236) - ]), - width: 2, + child: TextButton( + onPressed: () async { + await stopDeviceRecording(); + await provider.initialise(finalizedCallback: restartDeviceRecording); + provider.forceCompletionTimer = + Timer(Duration(seconds: provider.maxDuration), () async { + provider.finalize(); + }); + provider.updateStartedRecording(true); + }, + child: const Text( + 'Get Started', + style: TextStyle(color: Colors.white, fontSize: 16), + ), ), - borderRadius: BorderRadius.circular(12), ), - child: TextButton( - onPressed: () { - provider.close(); - widget.goNext(); - }, - child: const Text( - "All done!", - style: TextStyle(color: Colors.white, fontSize: 16), - ), + const SizedBox(height: 10), + ], + )) + : provider.profileCompleted + ? Container( + margin: const EdgeInsets.only(top: 40), + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 0), + decoration: BoxDecoration( + border: const GradientBoxBorder( + gradient: LinearGradient(colors: [ + Color.fromARGB(127, 208, 208, 208), + Color.fromARGB(127, 188, 99, 121), + Color.fromARGB(127, 86, 101, 182), + Color.fromARGB(127, 126, 190, 236) + ]), + width: 2, ), - ) - : provider.uploadingProfile - ? Padding( - padding: const EdgeInsets.only(top: 40.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const SizedBox( - height: 24, - width: 24, - child: Center( - child: CircularProgressIndicator( - valueColor: AlwaysStoppedAnimation(Colors.white), - ), - ), - ), - const SizedBox(width: 24), - Text(provider.loadingText, - style: const TextStyle(color: Colors.white, fontSize: 18)), - ], - ), - ) - : Column( - mainAxisSize: MainAxisSize.min, + borderRadius: BorderRadius.circular(12), + ), + child: TextButton( + onPressed: () { + widget.goNext(); + }, + child: const Text( + "All done!", + style: TextStyle(color: Colors.white, fontSize: 16), + ), + ), + ) + : provider.uploadingProfile + ? Padding( + padding: const EdgeInsets.only(top: 40.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, children: [ - const SizedBox(height: 30), - Text( - provider.message, - style: TextStyle(color: Colors.grey.shade300, fontSize: 14, height: 1.4), - textAlign: TextAlign.center, - ), - const SizedBox(height: 18), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 32), - child: Stack( - children: [ - LinearProgressIndicator( - value: provider.percentageCompleted, - backgroundColor: Colors.grey.shade300, - valueColor: const AlwaysStoppedAnimation(Colors.deepPurple), - ), - ], + const SizedBox( + height: 24, + width: 24, + child: Center( + child: CircularProgressIndicator( + valueColor: AlwaysStoppedAnimation(Colors.white), + ), ), ), - const SizedBox(height: 12), - Text( - '${(provider.percentageCompleted * 100).toInt()}%', - style: const TextStyle(color: Colors.white), - ), + const SizedBox(width: 24), + Text(provider.loadingText, + style: const TextStyle(color: Colors.white, fontSize: 18)), ], ), - ), + ) + : Column( + mainAxisSize: MainAxisSize.min, + children: [ + const SizedBox(height: 20), + provider.percentageCompleted > 0 + ? const SizedBox() + : const Text( + "Introduce\nyourself", + style: TextStyle(color: Colors.white, fontSize: 24, height: 1.4), + textAlign: TextAlign.center, + ), + const SizedBox(height: 12), + SizedBox( + width: MediaQuery.sizeOf(context).width * 0.9, + child: ProgressBarWithPercentage(progressValue: provider.percentageCompleted)), + const SizedBox(height: 14), + Text( + provider.message, + style: TextStyle(color: Colors.grey.shade300, fontSize: 14, height: 1.4), + textAlign: TextAlign.center, + ), + ], + ), (!provider.startedRecording) ? TextButton( onPressed: () { diff --git a/app/lib/pages/onboarding/wrapper.dart b/app/lib/pages/onboarding/wrapper.dart index 0d9ae59a0..6b87e2cf8 100644 --- a/app/lib/pages/onboarding/wrapper.dart +++ b/app/lib/pages/onboarding/wrapper.dart @@ -8,7 +8,6 @@ import 'package:friend_private/backend/schema/bt_device/bt_device.dart'; import 'package:friend_private/pages/home/page.dart'; import 'package:friend_private/pages/onboarding/auth.dart'; import 'package:friend_private/pages/onboarding/find_device/page.dart'; -import 'package:friend_private/pages/onboarding/memory_created_widget.dart'; import 'package:friend_private/pages/onboarding/name/name_widget.dart'; import 'package:friend_private/pages/onboarding/permissions/permissions_widget.dart'; import 'package:friend_private/pages/onboarding/speech_profile_widget.dart'; @@ -36,7 +35,7 @@ class _OnboardingWrapperState extends State with TickerProvid @override void initState() { //TODO: Change from tab controller to default controller and use provider (part of instabug cleanup) @mdmohsin7 - _controller = TabController(length: hasSpeechProfile ? 5 : 7, vsync: this); + _controller = TabController(length: hasSpeechProfile ? 5 : 6, vsync: this); _controller!.addListener(() => setState(() {})); WidgetsBinding.instance.addPostFrameCallback((_) async { if (isSignedIn()) { @@ -158,12 +157,12 @@ class _OnboardingWrapperState extends State with TickerProvid routeToPage(context, const HomePageWrapper(), replace: true); }, ), - MemoryCreatedWidget( - goNext: () { - // _goNext(); - MixpanelManager().onboardingStepCompleted('Memory Created'); - }, - ), + // MemoryCreatedWidget( + // goNext: () { + // // _goNext(); + // MixpanelManager().onboardingStepCompleted('Memory Created'); + // }, + // ), ]); } diff --git a/app/lib/pages/speech_profile/page.dart b/app/lib/pages/speech_profile/page.dart index 864b04c37..cd29c556b 100644 --- a/app/lib/pages/speech_profile/page.dart +++ b/app/lib/pages/speech_profile/page.dart @@ -16,6 +16,8 @@ import 'package:gradient_borders/box_borders/gradient_box_border.dart'; import 'package:intercom_flutter/intercom_flutter.dart'; import 'package:provider/provider.dart'; +import 'percentage_bar_progress.dart'; + class SpeechProfilePage extends StatefulWidget { final bool onbording; @@ -29,6 +31,7 @@ class _SpeechProfilePageState extends State with TickerProvid @override void initState() { WidgetsBinding.instance.addPostFrameCallback((timeStamp) async { + context.read().close(); await context.read().updateDevice(); }); @@ -114,7 +117,7 @@ class _SpeechProfilePageState extends State with TickerProvid Navigator.pop(context); }, () {}, - 'Invalid recording detected', + 'Multiple speakers detected', 'It seems like there are multiple speakers in the recording. Please make sure you are in a quiet location and try again.', okButtonText: 'Try Again', singleButton: true, @@ -208,21 +211,13 @@ class _SpeechProfilePageState extends State with TickerProvid ), body: Stack( children: [ - Align( + const Align( alignment: Alignment.topCenter, child: Padding( - padding: EdgeInsets.fromLTRB(16, 8, 16, 0), + padding: EdgeInsets.fromLTRB(24, 8, 24, 0), child: Column( children: [ - const DeviceAnimationWidget(sizeMultiplier: 0.2, animatedBackground: false), - !provider.startedRecording - ? const SizedBox(height: 0) - : const Text( - 'Tell your Friend\nabout yourself', - style: TextStyle( - color: Colors.white, fontSize: 18, fontWeight: FontWeight.w500, height: 1.4), - textAlign: TextAlign.center, - ), + DeviceAnimationWidget(animatedBackground: true), ], ), ), @@ -230,73 +225,89 @@ class _SpeechProfilePageState extends State with TickerProvid Align( alignment: Alignment.center, child: Padding( - padding: const EdgeInsets.fromLTRB(40, 0, 40, 48), + padding: const EdgeInsets.fromLTRB(40, 40, 40, 48), child: !provider.startedRecording - ? const Text( - 'Now, Omi needs to learn your voice to be able to recognise you.', - textAlign: TextAlign.center, - style: TextStyle( - color: Colors.white, - fontSize: 20, - height: 1.4, - fontWeight: FontWeight.w400, - ), + ? const Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SizedBox(height: 10), + Text( + 'Omi needs to learn your voice to be able to recognise you.', + textAlign: TextAlign.center, + style: TextStyle( + color: Colors.white, + fontSize: 20, + height: 1.4, + fontWeight: FontWeight.w400, + ), + ), + SizedBox(height: 20), + Text("Note: This only works in English", + style: TextStyle(color: Colors.white, fontSize: 16)), + ], ) : provider.text.isEmpty - ? const DeviceAnimationWidget( - sizeMultiplier: 0.7, - ) - : LayoutBuilder( - builder: (context, constraints) { - return ShaderMask( - shaderCallback: (bounds) { - if (provider.text.split(' ').length < 10) { - return const LinearGradient(colors: [Colors.white, Colors.white]) - .createShader(bounds); - } - return const LinearGradient( - colors: [Colors.transparent, Colors.white], - stops: [0.0, 0.5], - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - ).createShader(bounds); - }, - blendMode: BlendMode.dstIn, - child: SizedBox( - height: 120, - child: ListView( - controller: _scrollController, - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - children: [ - Text( - provider.text, - textAlign: TextAlign.center, - style: const TextStyle( - color: Colors.white, - fontSize: 20, - fontWeight: FontWeight.w400, - height: 1.5, + ? (provider.percentageCompleted > 0 + ? const SizedBox() + : const Text( + "Introduce\nyourself", + style: TextStyle(color: Colors.white, fontSize: 24, height: 1.4), + textAlign: TextAlign.center, + )) + : Padding( + padding: const EdgeInsets.only(top: 80.0), + child: LayoutBuilder( + builder: (context, constraints) { + return ShaderMask( + shaderCallback: (bounds) { + if (provider.text.split(' ').length < 10) { + return const LinearGradient(colors: [Colors.white, Colors.white]) + .createShader(bounds); + } + return const LinearGradient( + colors: [Colors.transparent, Colors.white], + stops: [0.0, 0.5], + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + ).createShader(bounds); + }, + blendMode: BlendMode.dstIn, + child: SizedBox( + height: 130, + child: ListView( + controller: _scrollController, + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + children: [ + Text( + provider.text, + textAlign: TextAlign.center, + style: const TextStyle( + color: Colors.white, + fontSize: 20, + fontWeight: FontWeight.w400, + height: 1.5, + ), ), - ), - ], + ], + ), ), - ), - ); - }, + ); + }, + ), ), ), ), Align( alignment: Alignment.bottomCenter, child: Padding( - padding: const EdgeInsets.fromLTRB(24, 0, 24, 48), + padding: const EdgeInsets.fromLTRB(0, 0, 0, 48), child: !provider.startedRecording ? Column( mainAxisAlignment: MainAxisAlignment.end, children: [ provider.isInitialising - ? CircularProgressIndicator( + ? const CircularProgressIndicator( color: Colors.white, ) : MaterialButton( @@ -412,32 +423,18 @@ class _SpeechProfilePageState extends State with TickerProvid : Column( mainAxisSize: MainAxisSize.min, children: [ + const SizedBox(height: 24), + SizedBox( + width: MediaQuery.sizeOf(context).width * 0.9, + child: ProgressBarWithPercentage(progressValue: provider.percentageCompleted), + ), + const SizedBox(height: 18), Text( provider.message, style: TextStyle(color: Colors.grey.shade300, fontSize: 14, height: 1.4), textAlign: TextAlign.center, ), - const SizedBox(height: 24), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 32), - child: Stack( - children: [ - // LinearProgressIndicator( - // backgroundColor: Colors.grey[300], - // valueColor: AlwaysStoppedAnimation(Colors.grey.withOpacity(0.3)), - // ), - LinearProgressIndicator( - value: provider.percentageCompleted, - backgroundColor: - Colors.grey.shade300, // Make sure background is transparent - valueColor: const AlwaysStoppedAnimation(Colors.deepPurple), - ), - ], - ), - ), - const SizedBox(height: 12), - Text('${(provider.percentageCompleted * 100).toInt()}%', - style: const TextStyle(color: Colors.white)), + const SizedBox(height: 30), ], ), ), diff --git a/app/lib/pages/speech_profile/percentage_bar_progress.dart b/app/lib/pages/speech_profile/percentage_bar_progress.dart new file mode 100644 index 000000000..57574fb29 --- /dev/null +++ b/app/lib/pages/speech_profile/percentage_bar_progress.dart @@ -0,0 +1,133 @@ +import 'package:flutter/material.dart'; + +class ProgressBarWithPercentage extends StatefulWidget { + final double progressValue; + + const ProgressBarWithPercentage({super.key, required this.progressValue}); + @override + _ProgressBarWithPercentageState createState() => _ProgressBarWithPercentageState(); +} + +class _ProgressBarWithPercentageState extends State { + @override + Widget build(BuildContext context) { + return SizedBox( + height: 60, + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.end, + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: EdgeInsets.only(left: widget.progressValue > 0.05 ? 15.0 : 20.0), + child: SizedBox( + height: 46, + child: Stack( + children: [ + Positioned( + left: ((MediaQuery.sizeOf(context).width * 0.72) * + (double.parse(widget.progressValue.toStringAsFixed(2)))), + child: ProgressBubble( + content: '${(widget.progressValue * 100).toInt()}%', + ), + ), + ], + ), + ), + ), + SizedBox( + width: MediaQuery.sizeOf(context).width * 0.72, + height: 8, + child: ClipRRect( + borderRadius: const BorderRadius.all(Radius.circular(10)), + child: LinearProgressIndicator( + value: double.parse(widget.progressValue.toStringAsFixed(2)), + backgroundColor: Colors.grey.shade300, + valueColor: const AlwaysStoppedAnimation(Colors.deepPurple), + ), + ), + ), + ], + ), + ); + } +} + +class TrianglePainter extends CustomPainter { + final Color color; + final Color shadowColor; + final double triangleHeight; + final double triangleBaseWidth; + + TrianglePainter({ + required this.color, + required this.shadowColor, + this.triangleHeight = 10.0, // Default height + this.triangleBaseWidth = 10.0, // Default base width + }); + + @override + void paint(Canvas canvas, Size size) { + final paint = Paint() + ..color = color + ..style = PaintingStyle.fill; + + final trianglePath = Path() + // Move to the left point of the base of the triangle + ..moveTo(size.width / 2 - triangleBaseWidth / 2, 0) + // Draw a line to the right point of the base + ..lineTo(size.width / 2 + triangleBaseWidth / 2, 0) + // Draw a line to the tip of the triangle (height) + ..lineTo(size.width / 2, triangleHeight) + ..close(); + + // Draw triangle + canvas.drawPath(trianglePath, paint); + } + + @override + bool shouldRepaint(covariant CustomPainter oldDelegate) => false; +} + +class ProgressBubble extends StatelessWidget { + final String content; + final double triangleHeight; + + const ProgressBubble({ + super.key, + required this.content, + this.triangleHeight = 10, + }); + + @override + Widget build(BuildContext context) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + ), + child: Padding( + padding: const EdgeInsets.all(10), + child: Text( + content, + style: const TextStyle( + fontSize: 14, + color: Colors.black, + ), + ), + ), + ), + CustomPaint( + painter: TrianglePainter( + color: Colors.white, + shadowColor: Colors.grey.withOpacity(0.5), + ), + size: Size(10, triangleHeight), + ), + ], + ); + } +} diff --git a/app/lib/providers/speech_profile_provider.dart b/app/lib/providers/speech_profile_provider.dart index 2384debe6..fd1507ac0 100644 --- a/app/lib/providers/speech_profile_provider.dart +++ b/app/lib/providers/speech_profile_provider.dart @@ -24,8 +24,9 @@ class SpeechProfileProvider extends ChangeNotifier bool loading = false; BtDevice? device; - final targetWordsCount = 70; - final maxDuration = 90; + final targetWordsCount = 30; //TODO: 15 seems way too less + final maxDuration = 150; + StreamSubscription? connectionStateListener; List segments = []; double? streamStartedAtSecond; @@ -142,14 +143,18 @@ class SpeechProfileProvider extends ChangeNotifier if (uploadingProfile || profileCompleted) return; int duration = segments.isEmpty ? 0 : segments.last.end.toInt(); - if (duration < 5 || duration > 120) { - notifyError('INVALID_RECORDING'); + if (duration < 10 || duration > 155) { + if (percentageCompleted < 80) { + notifyError('NO_SPEECH'); + return; + } } String text = segments.map((e) => e.text).join(' ').trim(); if (text.split(' ').length < (targetWordsCount / 2)) { // 25 words notifyError('TOO_SHORT'); + return; } uploadingProfile = true; notifyListeners(); @@ -220,7 +225,7 @@ class SpeechProfileProvider extends ChangeNotifier }, ); debugPrint('speakerToWords: $speakerToWords'); - if (speakerToWords.values.every((element) => element / segments.length > 0.2)) { + if (speakerToWords.values.every((element) => element / segments.length > 0.08)) { notifyError('MULTIPLE_SPEAKERS'); } } @@ -230,6 +235,8 @@ class SpeechProfileProvider extends ChangeNotifier segments.clear(); streamStartedAtSecond = null; audioStorage.clearAudioBytes(); + text = ''; + percentageCompleted = 0; notifyListeners(); }