From 5002d35e9e9eb9bcff62d95f5182fbdcd87c107b Mon Sep 17 00:00:00 2001 From: Bernhard Zwahlen Date: Sun, 23 Mar 2025 13:20:18 -0500 Subject: [PATCH 1/8] testing vad as solution for audio chunking and better speaker id --- team_b/yappy/assets/models_config.json | 14 +++++ team_b/yappy/lib/services/speech_state.dart | 66 ++++++++++++++++++++- team_b/yappy/lib/services/vad_model.dart | 12 ++++ 3 files changed, 91 insertions(+), 1 deletion(-) create mode 100644 team_b/yappy/lib/services/vad_model.dart diff --git a/team_b/yappy/assets/models_config.json b/team_b/yappy/assets/models_config.json index fd6815a3..0f19de6d 100644 --- a/team_b/yappy/assets/models_config.json +++ b/team_b/yappy/assets/models_config.json @@ -14,6 +14,20 @@ ], "size": 25.3 }, + { + "id": "silero_vad", + "name": "Voice Activity Detection Model", + "url": "https://github.com/k2-fsa/sherpa-onnx/releases/download/asr-models/silero_vad.onnx", + "isCompressed": false, + "type": "vad", + "outputFiles": [ + { + "source": "silero_vad.onnx", + "destination": "vad_model.onnx" + } + ], + "size": 1.72 + }, { "id": "whisper_tiny", "name": "Offline Whisper Model (Tiny)", diff --git a/team_b/yappy/lib/services/speech_state.dart b/team_b/yappy/lib/services/speech_state.dart index 992c3c6d..a0071071 100644 --- a/team_b/yappy/lib/services/speech_state.dart +++ b/team_b/yappy/lib/services/speech_state.dart @@ -9,6 +9,7 @@ import 'utils.dart'; import 'online_model.dart'; import 'offline_model.dart'; import 'speaker_model.dart'; +import 'vad_model.dart'; import 'speech_isolate.dart'; Future createOnlineRecognizer() async { @@ -48,6 +49,29 @@ Future createSpeakerExtractor() async { return sherpa_onnx.SpeakerEmbeddingExtractor(config: config); } +Future createVoiceActivityDetector() async { + final type = 0; + + final model = await getVadModel(type: type); + final sileroConfig = sherpa_onnx.SileroVadModelConfig( + model: model, + threshold: 0.5, + minSilenceDuration: 0.4, + minSpeechDuration: 0.5, + windowSize: 512, + maxSpeechDuration: 10.0, + ); + + final vadConfig = sherpa_onnx.VadModelConfig( + sileroVad: sileroConfig, + numThreads: 2, + provider: 'cpu', + debug: false, + ); + + return sherpa_onnx.VoiceActivityDetector(config: vadConfig, bufferSizeInSeconds: 10); +} + class Conversation { final List segments; final String audioFilePath; @@ -181,6 +205,9 @@ class SpeechState extends ChangeNotifier { // Second pass - offline processing SpeechProcessingIsolate? speechIsolate; + // Voice Activity Detector + sherpa_onnx.VoiceActivityDetector? vad; + List allAudioSamples = []; // Buffer for collecting samples between endpoints List currentSegmentSamples = []; @@ -204,6 +231,9 @@ class SpeechState extends ChangeNotifier { onlineRecognizer = await createOnlineRecognizer(); onlineStream = onlineRecognizer?.createStream(); + // init vad + vad = await createVoiceActivityDetector(); + // Initialize the isolate with configuration speechIsolate = SpeechProcessingIsolate(); final offlineModelConfig = await getOfflineModelConfig(type: 0); @@ -423,6 +453,8 @@ Future processSegmentOffline(AudioSegment segment) async { final recordStream = await audioRecorder.startStream(config); currentSegmentSamples.clear(); allAudioSamples.clear(); + bool isCurrentlySpeaking = false; + double speechStartTime = 0.0; currentTimestamp = 0.0; currentIndex = 0; @@ -477,7 +509,37 @@ Future processSegmentOffline(AudioSegment segment) async { _updateDisplayText(); } - if (onlineRecognizer!.isEndpoint(onlineStream!)) { + // Check for endpointing with VAD + // Process through VAD in window-sized chunks + final windowSize = vad!.config.sileroVad.windowSize; + + // Process as many complete windows as we can + int offset = 0; + while (offset + windowSize <= samplesFloat32.length) { + final windowBuffer = Float32List.sublistView(samplesFloat32, offset, offset + windowSize); + + vad!.acceptWaveform(windowBuffer); + offset += windowSize; + + // Check if speech is detected + while (!vad!.isEmpty()) { + final segment = vad!.front(); + + // Here you could also: + // 1. Send to a speech recognizer (like Whisper) for transcription + // 2. Save to a file + // 3. Process in some other way + + // Log the timestamp for debugging + debugPrint('💬 Speech detected: ${segment.start} to $currentTimestamp'); + + // Remove the processed segment from the VAD queue + vad!.pop(); + } + } + + // if (onlineRecognizer!.isEndpoint(onlineStream!)) { + if (!vad!.isDetected()) { // Store the current segment for offline processing //ISSUE IS HERE, need to use recognizedSegments.LastOrNull like before, or integrate VAD @@ -510,6 +572,7 @@ Future processSegmentOffline(AudioSegment segment) async { onlineRecognizer!.reset(onlineStream!); currentSegmentSamples.clear(); currentIndex += 1; + vad?.flush(); } }, onError: (error) { @@ -704,6 +767,7 @@ Future processSegmentOffline(AudioSegment segment) async { audioRecorder.dispose(); onlineStream?.free(); onlineRecognizer?.free(); + vad?.free(); speechIsolate?.dispose(); super.dispose(); } diff --git a/team_b/yappy/lib/services/vad_model.dart b/team_b/yappy/lib/services/vad_model.dart new file mode 100644 index 00000000..768c0888 --- /dev/null +++ b/team_b/yappy/lib/services/vad_model.dart @@ -0,0 +1,12 @@ +import './model_manager.dart'; + +Future getVadModel({required int type}) async { + final modelManager = ModelManager(); + + switch (type) { + case 0: + return await modelManager.getModelPath('vad', 'vad_model.onnx'); + default: + throw ArgumentError('Unsupported type: $type'); + } +} \ No newline at end of file From b6a064ab0a729722363d345d5b77fbb2b8b3b633 Mon Sep 17 00:00:00 2001 From: Bernhard Zwahlen Date: Sun, 23 Mar 2025 14:44:21 -0500 Subject: [PATCH 2/8] improvements --- team_b/yappy/lib/services/speech_state.dart | 176 ++++++++++---------- 1 file changed, 90 insertions(+), 86 deletions(-) diff --git a/team_b/yappy/lib/services/speech_state.dart b/team_b/yappy/lib/services/speech_state.dart index a0071071..20a07025 100644 --- a/team_b/yappy/lib/services/speech_state.dart +++ b/team_b/yappy/lib/services/speech_state.dart @@ -41,7 +41,7 @@ Future createSpeakerExtractor() async { final model = await getSpeakerModel(type: type); final config = sherpa_onnx.SpeakerEmbeddingExtractorConfig( model: model, - numThreads: 2, + numThreads: 1, debug: false, provider: 'cpu', ); @@ -56,15 +56,15 @@ Future createVoiceActivityDetector() async { final sileroConfig = sherpa_onnx.SileroVadModelConfig( model: model, threshold: 0.5, - minSilenceDuration: 0.4, - minSpeechDuration: 0.5, + minSilenceDuration: 0.2, + minSpeechDuration: 0.2, windowSize: 512, - maxSpeechDuration: 10.0, + maxSpeechDuration: 5.0, ); final vadConfig = sherpa_onnx.VadModelConfig( sileroVad: sileroConfig, - numThreads: 2, + numThreads: 1, provider: 'cpu', debug: false, ); @@ -437,11 +437,9 @@ Future processSegmentOffline(AudioSegment segment) async { try { if (await audioRecorder.hasPermission()) { - // Reset speakers for new recording + // Reset for new recording currentSpeakerCount = 0; recognizedSegments.clear(); - - // Create a path for saving the recording recordingFilePath = await _createRecordingFilePath(); const config = RecordConfig( @@ -451,128 +449,134 @@ Future processSegmentOffline(AudioSegment segment) async { ); final recordStream = await audioRecorder.startStream(config); - currentSegmentSamples.clear(); allAudioSamples.clear(); - bool isCurrentlySpeaking = false; - double speechStartTime = 0.0; currentTimestamp = 0.0; currentIndex = 0; + + // State tracking variables + bool isCurrentlySpeaking = false; + double speechStartTime = 0.0; + currentSegmentSamples.clear(); recordState = RecordState.record; - controller.value = TextEditingValue( - text: "Listening..." - ); + controller.value = TextEditingValue(text: "Listening..."); notifyListeners(); recordStream.listen( (data) { final samplesFloat32 = convertBytesToFloat32(Uint8List.fromList(data)); - - // Add samples to current segment buffer - currentSegmentSamples.add(samplesFloat32); + + // Always add to complete recording and update timestamp allAudioSamples.add(samplesFloat32); - - // Update current timestamp based on number of samples currentTimestamp += samplesFloat32.length / sampleRate; - - // ALYS CODE TO HELP UPDATE THE AUDIOWAVE SAMPLES + + // Update audio visualization audioSamplesNotifier.value = samplesFloat32 .map((e) => (e * 100000).toInt()) .toList(); - onlineStream!.acceptWaveform( - samples: samplesFloat32, - sampleRate: sampleRate - ); - - while (onlineRecognizer!.isReady(onlineStream!)) { - onlineRecognizer!.decode(onlineStream!); - } - - final text = onlineRecognizer!.getResult(onlineStream!).text; - - if (text.isNotEmpty) { - // Update or add the current segment - final existingSegmentIndex = recognizedSegments.indexWhere((s) => s.index == currentIndex); - - if (existingSegmentIndex != -1) { - // Update existing segment - recognizedSegments[existingSegmentIndex].text = text; - // debugPrint('Updated segment $currentIndex with text: "$text"'); - } else { - // Add new segment - debugPrint('Adding new segment $currentIndex with text: "$text"'); - _addRecognizedSegment(text, currentTimestamp); - } - - // Always update display when we have new text - _updateDisplayText(); - } - - // Check for endpointing with VAD - // Process through VAD in window-sized chunks + // Process audio through VAD final windowSize = vad!.config.sileroVad.windowSize; - - // Process as many complete windows as we can int offset = 0; while (offset + windowSize <= samplesFloat32.length) { final windowBuffer = Float32List.sublistView(samplesFloat32, offset, offset + windowSize); - vad!.acceptWaveform(windowBuffer); offset += windowSize; - - // Check if speech is detected + + // Process any complete segments from VAD while (!vad!.isEmpty()) { final segment = vad!.front(); - - // Here you could also: - // 1. Send to a speech recognizer (like Whisper) for transcription - // 2. Save to a file - // 3. Process in some other way - - // Log the timestamp for debugging - debugPrint('💬 Speech detected: ${segment.start} to $currentTimestamp'); - - // Remove the processed segment from the VAD queue + debugPrint('💬 VAD segment at: ${segment.start / sampleRate}s - ${currentTimestamp}s'); vad!.pop(); } } - // if (onlineRecognizer!.isEndpoint(onlineStream!)) { - if (!vad!.isDetected()) { - // Store the current segment for offline processing - - //ISSUE IS HERE, need to use recognizedSegments.LastOrNull like before, or integrate VAD - if (currentSegmentSamples.isNotEmpty && recognizedSegments.lastOrNull != null - ) { - // Combine all Float32Lists into a single one + // Check current VAD state + bool speechDetected = vad!.isDetected(); + + // TRANSITION: Silence → Speech (START COLLECTING) + if (!isCurrentlySpeaking && speechDetected) { + debugPrint('🎤 Speech started at: $currentTimestamp'); + isCurrentlySpeaking = true; + speechStartTime = currentTimestamp; + currentSegmentSamples.clear(); // Start fresh collection + + // Reset the recognizer for a new segment + onlineRecognizer!.reset(onlineStream!); + } + + // DURING SPEECH: Collect and process audio + if (isCurrentlySpeaking) { + // Add samples to the current segment + currentSegmentSamples.add(samplesFloat32); + + // Process with online recognizer for real-time feedback + onlineStream!.acceptWaveform( + samples: samplesFloat32, + sampleRate: sampleRate + ); + + while (onlineRecognizer!.isReady(onlineStream!)) { + onlineRecognizer!.decode(onlineStream!); + } + + final text = onlineRecognizer!.getResult(onlineStream!).text; + + // Update display with current recognition + if (text.isNotEmpty) { + final existingSegmentIndex = recognizedSegments.indexWhere((s) => s.index == currentIndex); + + if (existingSegmentIndex != -1) { + // Update existing segment + recognizedSegments[existingSegmentIndex].text = text; + } else { + // Add new segment + debugPrint('Adding new segment $currentIndex with text: "$text"'); + _addRecognizedSegment(text, speechStartTime); + } + + _updateDisplayText(); + } + } + + // TRANSITION: Speech → Silence (SAVE SEGMENT & STOP COLLECTING) + if (isCurrentlySpeaking && !speechDetected) { + debugPrint('🔇 Speech ended at: $currentTimestamp, duration: ${currentTimestamp - speechStartTime}'); + isCurrentlySpeaking = false; + + // Only process if we collected enough speech data + if (currentSegmentSamples.isNotEmpty && recognizedSegments.lastOrNull != null) { + // Combine all samples into a single Float32List final combinedSamples = Float32List(currentSegmentSamples.fold( 0, (sum, list) => sum + list.length)); - var offset = 0; + + var sampleOffset = 0; for (var samples in currentSegmentSamples) { - combinedSamples.setRange(offset, offset + samples.length, samples); - offset += samples.length; + combinedSamples.setRange(sampleOffset, sampleOffset + samples.length, samples); + sampleOffset += samples.length; } - final segmentStart = recognizedSegments.lastOrNull?.start ?? 0.0; - + // Create and add a new audio segment for background processing pendingSegments.add(AudioSegment( samples: combinedSamples, sampleRate: sampleRate, index: currentIndex, - start: segmentStart, + start: speechStartTime, end: currentTimestamp, )); - // Process with online recognizer in the background + // Process with offline recognizer in the background processPendingSegments(); + + // Increment for next segment + currentIndex += 1; } - // Reset for next segment - onlineRecognizer!.reset(onlineStream!); - currentSegmentSamples.clear(); - currentIndex += 1; + // Clear VAD buffer - flush any pending segments vad?.flush(); + + // During silence we don't collect samples - they're effectively discarded + // until the next speech segment begins } }, onError: (error) { From 75e4c3d582a77c2fcfeabbf88ad58a8bbf1c665e Mon Sep 17 00:00:00 2001 From: Bernhard Zwahlen Date: Sun, 23 Mar 2025 17:09:12 -0500 Subject: [PATCH 3/8] better, but flaky --- team_b/yappy/assets/models_config.json | 4 +- team_b/yappy/lib/services/speech_state.dart | 75 ++++++++++----------- 2 files changed, 39 insertions(+), 40 deletions(-) diff --git a/team_b/yappy/assets/models_config.json b/team_b/yappy/assets/models_config.json index 0f19de6d..01c0f15b 100644 --- a/team_b/yappy/assets/models_config.json +++ b/team_b/yappy/assets/models_config.json @@ -17,7 +17,7 @@ { "id": "silero_vad", "name": "Voice Activity Detection Model", - "url": "https://github.com/k2-fsa/sherpa-onnx/releases/download/asr-models/silero_vad.onnx", + "url": "https://github.com/k2-fsa/sherpa-onnx/releases/download/asr-models/silero_vad_v5.onnx", "isCompressed": false, "type": "vad", "outputFiles": [ @@ -26,7 +26,7 @@ "destination": "vad_model.onnx" } ], - "size": 1.72 + "size": 2.2 }, { "id": "whisper_tiny", diff --git a/team_b/yappy/lib/services/speech_state.dart b/team_b/yappy/lib/services/speech_state.dart index 20a07025..2d34c010 100644 --- a/team_b/yappy/lib/services/speech_state.dart +++ b/team_b/yappy/lib/services/speech_state.dart @@ -41,7 +41,7 @@ Future createSpeakerExtractor() async { final model = await getSpeakerModel(type: type); final config = sherpa_onnx.SpeakerEmbeddingExtractorConfig( model: model, - numThreads: 1, + numThreads: 2, debug: false, provider: 'cpu', ); @@ -56,15 +56,15 @@ Future createVoiceActivityDetector() async { final sileroConfig = sherpa_onnx.SileroVadModelConfig( model: model, threshold: 0.5, - minSilenceDuration: 0.2, - minSpeechDuration: 0.2, + minSilenceDuration: 0.25, + minSpeechDuration: 0.15, windowSize: 512, - maxSpeechDuration: 5.0, + maxSpeechDuration: 10.0, ); final vadConfig = sherpa_onnx.VadModelConfig( sileroVad: sileroConfig, - numThreads: 1, + numThreads: 2, provider: 'cpu', debug: false, ); @@ -482,13 +482,6 @@ Future processSegmentOffline(AudioSegment segment) async { final windowBuffer = Float32List.sublistView(samplesFloat32, offset, offset + windowSize); vad!.acceptWaveform(windowBuffer); offset += windowSize; - - // Process any complete segments from VAD - while (!vad!.isEmpty()) { - final segment = vad!.front(); - debugPrint('💬 VAD segment at: ${segment.start / sampleRate}s - ${currentTimestamp}s'); - vad!.pop(); - } } // Check current VAD state @@ -523,20 +516,19 @@ Future processSegmentOffline(AudioSegment segment) async { final text = onlineRecognizer!.getResult(onlineStream!).text; // Update display with current recognition - if (text.isNotEmpty) { - final existingSegmentIndex = recognizedSegments.indexWhere((s) => s.index == currentIndex); - - if (existingSegmentIndex != -1) { - // Update existing segment - recognizedSegments[existingSegmentIndex].text = text; - } else { - // Add new segment - debugPrint('Adding new segment $currentIndex with text: "$text"'); - _addRecognizedSegment(text, speechStartTime); - } - - _updateDisplayText(); + final existingSegmentIndex = recognizedSegments.indexWhere((s) => s.index == currentIndex); + + if (existingSegmentIndex != -1) { + // Update existing segment + debugPrint('Updated segment #$currentIndex of ${recognizedSegments.length}'); + recognizedSegments[existingSegmentIndex].text = text.isEmpty ? recognizedSegments[existingSegmentIndex].text : text; + } else { + // Add new segment + debugPrint('Adding new segment $currentIndex with text: "$text"'); + _addRecognizedSegment(text, speechStartTime); } + + _updateDisplayText(); } // TRANSITION: Speech → Silence (SAVE SEGMENT & STOP COLLECTING) @@ -544,21 +536,21 @@ Future processSegmentOffline(AudioSegment segment) async { debugPrint('🔇 Speech ended at: $currentTimestamp, duration: ${currentTimestamp - speechStartTime}'); isCurrentlySpeaking = false; - // Only process if we collected enough speech data - if (currentSegmentSamples.isNotEmpty && recognizedSegments.lastOrNull != null) { + // // Only process if we collected enough speech data + // if (currentSegmentSamples.isNotEmpty && recognizedSegments.lastOrNull != null) { // Combine all samples into a single Float32List - final combinedSamples = Float32List(currentSegmentSamples.fold( - 0, (sum, list) => sum + list.length)); + // final combinedSamples = Float32List(currentSegmentSamples.fold( + // 0, (sum, list) => sum + list.length)); - var sampleOffset = 0; - for (var samples in currentSegmentSamples) { - combinedSamples.setRange(sampleOffset, sampleOffset + samples.length, samples); - sampleOffset += samples.length; - } + // var sampleOffset = 0; + // for (var samples in currentSegmentSamples) { + // combinedSamples.setRange(sampleOffset, sampleOffset + samples.length, samples); + // sampleOffset += samples.length; + // } // Create and add a new audio segment for background processing pendingSegments.add(AudioSegment( - samples: combinedSamples, + samples: vad!.front().samples, sampleRate: sampleRate, index: currentIndex, start: speechStartTime, @@ -570,20 +562,27 @@ Future processSegmentOffline(AudioSegment segment) async { // Increment for next segment currentIndex += 1; - } - + // } + vad!.pop(); // Clear VAD buffer - flush any pending segments vad?.flush(); // During silence we don't collect samples - they're effectively discarded // until the next speech segment begins } + + // Process any complete segments from VAD + while (!vad!.isEmpty()) { + final segment = vad!.front(); + debugPrint('💬 VAD segment at: ${segment.start / sampleRate}s - ${currentTimestamp}s'); + vad!.pop(); + } }, onError: (error) { debugPrint('Error from audio stream: $error'); }, onDone: () { - debugPrint('Audio stream done'); + debugPrint('Audio stream done; ${recognizedSegments.length} segments with $currentSpeakerCount speakers'); }, ); } From 8a517b2092c10bcb83a8c46da8b9149562b27587 Mon Sep 17 00:00:00 2001 From: Bernhard Zwahlen Date: Sun, 23 Mar 2025 19:29:12 -0500 Subject: [PATCH 4/8] about as good as I can get it --- team_b/yappy/lib/services/speech_isolate.dart | 5 +- team_b/yappy/lib/services/speech_state.dart | 64 ++++++++----------- 2 files changed, 28 insertions(+), 41 deletions(-) diff --git a/team_b/yappy/lib/services/speech_isolate.dart b/team_b/yappy/lib/services/speech_isolate.dart index 5860c44e..1edd5c12 100644 --- a/team_b/yappy/lib/services/speech_isolate.dart +++ b/team_b/yappy/lib/services/speech_isolate.dart @@ -256,7 +256,7 @@ class SpeechProcessingIsolate { debugPrint('Isolate: Recognition result: "${result.text}"'); // Skip speaker identification if result is empty - if (result.text.trim().isEmpty) { + if (result.text.trim().isEmpty || result.text.trim().startsWith(RegExp(r'[\[\(]'))) { sendPort.send(ProcessSegmentResult( segmentIndex: message.segmentIndex, text: result.text, @@ -279,7 +279,8 @@ class SpeechProcessingIsolate { final embedding = speakerExtractor.compute(speakerStream); // Search for matching speaker - final threshold = 0.5; + // Adjust threshold lower for better accuracy + final threshold = 0.2; var speakerId = speakerManager.search(embedding: embedding, threshold: threshold); int newSpeakerCount = currentSpeakerCount; diff --git a/team_b/yappy/lib/services/speech_state.dart b/team_b/yappy/lib/services/speech_state.dart index 2d34c010..e8ea7d8c 100644 --- a/team_b/yappy/lib/services/speech_state.dart +++ b/team_b/yappy/lib/services/speech_state.dart @@ -57,9 +57,9 @@ Future createVoiceActivityDetector() async { model: model, threshold: 0.5, minSilenceDuration: 0.25, - minSpeechDuration: 0.15, + minSpeechDuration: 0.1, windowSize: 512, - maxSpeechDuration: 10.0, + maxSpeechDuration: 10.0, ); final vadConfig = sherpa_onnx.VadModelConfig( @@ -489,7 +489,7 @@ Future processSegmentOffline(AudioSegment segment) async { // TRANSITION: Silence → Speech (START COLLECTING) if (!isCurrentlySpeaking && speechDetected) { - debugPrint('🎤 Speech started at: $currentTimestamp'); + debugPrint('🎙️ Speech started at: $currentTimestamp'); isCurrentlySpeaking = true; speechStartTime = currentTimestamp; currentSegmentSamples.clear(); // Start fresh collection @@ -536,36 +536,23 @@ Future processSegmentOffline(AudioSegment segment) async { debugPrint('🔇 Speech ended at: $currentTimestamp, duration: ${currentTimestamp - speechStartTime}'); isCurrentlySpeaking = false; - // // Only process if we collected enough speech data - // if (currentSegmentSamples.isNotEmpty && recognizedSegments.lastOrNull != null) { - // Combine all samples into a single Float32List - // final combinedSamples = Float32List(currentSegmentSamples.fold( - // 0, (sum, list) => sum + list.length)); - - // var sampleOffset = 0; - // for (var samples in currentSegmentSamples) { - // combinedSamples.setRange(sampleOffset, sampleOffset + samples.length, samples); - // sampleOffset += samples.length; - // } - - // Create and add a new audio segment for background processing - pendingSegments.add(AudioSegment( - samples: vad!.front().samples, - sampleRate: sampleRate, - index: currentIndex, - start: speechStartTime, - end: currentTimestamp, - )); - - // Process with offline recognizer in the background - processPendingSegments(); - - // Increment for next segment - currentIndex += 1; - // } + // Create and add a new audio segment for background processing + pendingSegments.add(AudioSegment( + samples: vad!.front().samples, + sampleRate: sampleRate, + index: currentIndex, + start: speechStartTime, + end: currentTimestamp, + )); + + // Process with offline recognizer in the background + processPendingSegments(); + + // Increment for next segment + currentIndex += 1; + + // Clear current segment samples vad!.pop(); - // Clear VAD buffer - flush any pending segments - vad?.flush(); // During silence we don't collect samples - they're effectively discarded // until the next speech segment begins @@ -582,7 +569,9 @@ Future processSegmentOffline(AudioSegment segment) async { debugPrint('Error from audio stream: $error'); }, onDone: () { - debugPrint('Audio stream done; ${recognizedSegments.length} segments with $currentSpeakerCount speakers'); + debugPrint('🫳🎤 Audio stream done; ${recognizedSegments.length} segments with $currentSpeakerCount speakers'); + // Clear VAD buffer - flush any pending segments + vad?.flush(); }, ); } @@ -690,6 +679,7 @@ Future processSegmentOffline(AudioSegment segment) async { Future createConversation() async { // Ensure WAV file is saved + debugPrint('Saving WAV file'); await saveWavFile(); // Create conversation object @@ -742,12 +732,8 @@ Future processSegmentOffline(AudioSegment segment) async { // Process final segments debugPrint('Processing final segments with offline recognizer'); await processPendingSegments(); - - // Save the recording as a WAV file - debugPrint('Saving WAV file'); - await saveWavFile(); - - // Create conversation object + + // Create conversation object (also saves the WAV file) debugPrint('Creating conversation object'); lastConversation = await createConversation(); From 169f2ac9ad23292bebd8f127d61ffdfdeb1a3145 Mon Sep 17 00:00:00 2001 From: Bernhard Zwahlen Date: Sun, 23 Mar 2025 20:32:39 -0500 Subject: [PATCH 5/8] swapped speaker recognition model --- team_b/yappy/assets/models_config.json | 8 ++++---- team_b/yappy/lib/services/speech_isolate.dart | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/team_b/yappy/assets/models_config.json b/team_b/yappy/assets/models_config.json index 01c0f15b..55cc2d8a 100644 --- a/team_b/yappy/assets/models_config.json +++ b/team_b/yappy/assets/models_config.json @@ -3,16 +3,16 @@ { "id": "speaker_recognition", "name": "Speaker Recognition Model", - "url": "https://github.com/k2-fsa/sherpa-onnx/releases/download/speaker-recongition-models/3dspeaker_speech_eres2net_sv_en_voxceleb_16k.onnx", + "url": "https://github.com/k2-fsa/sherpa-onnx/releases/download/speaker-recongition-models/nemo_en_speakerverification_speakernet.onnx", "isCompressed": false, "type": "speaker", "outputFiles": [ { - "source": "3dspeaker_speech_eres2net_sv_en_voxceleb_16k.onnx", + "source": "nemo_en_speakerverification_speakernet.onnx", "destination": "speaker_model.onnx" } ], - "size": 25.3 + "size": 22.3 }, { "id": "silero_vad", @@ -22,7 +22,7 @@ "type": "vad", "outputFiles": [ { - "source": "silero_vad.onnx", + "source": "silero_vad_v5.onnx", "destination": "vad_model.onnx" } ], diff --git a/team_b/yappy/lib/services/speech_isolate.dart b/team_b/yappy/lib/services/speech_isolate.dart index 1edd5c12..b240a6b5 100644 --- a/team_b/yappy/lib/services/speech_isolate.dart +++ b/team_b/yappy/lib/services/speech_isolate.dart @@ -280,7 +280,7 @@ class SpeechProcessingIsolate { // Search for matching speaker // Adjust threshold lower for better accuracy - final threshold = 0.2; + final threshold = 0.3; var speakerId = speakerManager.search(embedding: embedding, threshold: threshold); int newSpeakerCount = currentSpeakerCount; From f3df1f17d2ed123555236a059abd0f2ff375d57b Mon Sep 17 00:00:00 2001 From: Bernhard Zwahlen Date: Mon, 24 Mar 2025 16:33:56 -0500 Subject: [PATCH 6/8] fix, sorry --- team_b/yappy/assets/models_config.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/team_b/yappy/assets/models_config.json b/team_b/yappy/assets/models_config.json index 55cc2d8a..00b4f1c1 100644 --- a/team_b/yappy/assets/models_config.json +++ b/team_b/yappy/assets/models_config.json @@ -17,12 +17,12 @@ { "id": "silero_vad", "name": "Voice Activity Detection Model", - "url": "https://github.com/k2-fsa/sherpa-onnx/releases/download/asr-models/silero_vad_v5.onnx", + "url": "https://github.com/k2-fsa/sherpa-onnx/releases/download/asr-models/silero_vad.onnx", "isCompressed": false, "type": "vad", "outputFiles": [ { - "source": "silero_vad_v5.onnx", + "source": "silero_vad.onnx", "destination": "vad_model.onnx" } ], From d410bdfb1bc083af1405ac30cb0e73b6b6e6b583 Mon Sep 17 00:00:00 2001 From: Aly Date: Thu, 27 Mar 2025 19:33:20 -0400 Subject: [PATCH 7/8] Created a light and dark mode toggle --- team_b/yappy/lib/audiowave_widget.dart | 1 + team_b/yappy/lib/contact_page.dart | 43 +++++-- team_b/yappy/lib/help.dart | 24 ++-- team_b/yappy/lib/home_page.dart | 2 - team_b/yappy/lib/industry_menu.dart | 25 ++-- team_b/yappy/lib/main.dart | 26 ++-- team_b/yappy/lib/mechanic.dart | 1 - team_b/yappy/lib/medical_doctor.dart | 1 - team_b/yappy/lib/medical_patient.dart | 1 - team_b/yappy/lib/restaurant.dart | 1 - team_b/yappy/lib/search_bar_widget.dart | 1 - team_b/yappy/lib/settings_page.dart | 15 ++- team_b/yappy/lib/theme_provider.dart | 61 ++++++++++ team_b/yappy/lib/tool_bar.dart | 151 ++++++++++++------------ team_b/yappy/lib/transcription_box.dart | 9 +- team_b/yappy/pubspec.yaml | 1 + 16 files changed, 238 insertions(+), 125 deletions(-) create mode 100644 team_b/yappy/lib/theme_provider.dart diff --git a/team_b/yappy/lib/audiowave_widget.dart b/team_b/yappy/lib/audiowave_widget.dart index ea242f69..c1426463 100644 --- a/team_b/yappy/lib/audiowave_widget.dart +++ b/team_b/yappy/lib/audiowave_widget.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'dart:math'; import 'services/speech_state.dart'; + class AudiowaveWidget extends StatelessWidget { final SpeechState speechState; diff --git a/team_b/yappy/lib/contact_page.dart b/team_b/yappy/lib/contact_page.dart index e69a2601..5742b3b2 100644 --- a/team_b/yappy/lib/contact_page.dart +++ b/team_b/yappy/lib/contact_page.dart @@ -1,5 +1,7 @@ import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; import 'package:yappy/tool_bar.dart'; +import 'package:yappy/theme_provider.dart'; //**************************************************************** */ //**************************************************************** */ @@ -10,6 +12,7 @@ import 'package:yappy/tool_bar.dart'; //**************************************************************** */ //**************************************************************** */ //**************************************************************** */ + class ContactApp extends StatelessWidget { const ContactApp({super.key}); @@ -25,13 +28,14 @@ class ContactPage extends StatelessWidget { final TextEditingController nameController = TextEditingController(); final TextEditingController emailController = TextEditingController(); final TextEditingController messageController = TextEditingController(); -//Creates a page for the Contact Us page -//The page will contain a form for the user to input their name, email, and message + ContactPage({super.key}); + @override Widget build(BuildContext context) { + final themeProvider = Provider.of(context); + return Scaffold( - backgroundColor: const Color.fromARGB(255, 0, 0, 0), appBar: PreferredSize( preferredSize: Size.fromHeight(140), child: ToolBar(), @@ -47,52 +51,65 @@ class ContactPage extends StatelessWidget { style: TextStyle( fontSize: 24, fontWeight: FontWeight.bold, - color: Colors.white, + color: themeProvider.isDarkMode ? Colors.white : Colors.black, ), ), + SizedBox(height: 16), TextField( controller: nameController, decoration: InputDecoration( labelText: 'Name', - labelStyle: TextStyle(color: Colors.white), + labelStyle: TextStyle( + color: themeProvider.isDarkMode ? Colors.white : Colors.black, + ), enabledBorder: OutlineInputBorder( - borderSide: BorderSide(color: Colors.white), + borderSide: BorderSide( + color: themeProvider.isDarkMode ? Colors.white : Colors.black, + ), ), focusedBorder: OutlineInputBorder( borderSide: BorderSide(color: Colors.blue), ), ), - style: TextStyle(color: Colors.white), + style: TextStyle(color: themeProvider.isDarkMode ? Colors.white : Colors.black), ), SizedBox(height: 16), TextField( controller: emailController, decoration: InputDecoration( labelText: 'Email', - labelStyle: TextStyle(color: Colors.white), + labelStyle: TextStyle( + color: themeProvider.isDarkMode ? Colors.white : Colors.black, + ), enabledBorder: OutlineInputBorder( - borderSide: BorderSide(color: Colors.white), + borderSide: BorderSide( + color: themeProvider.isDarkMode ? Colors.white : Colors.black, + ), ), focusedBorder: OutlineInputBorder( borderSide: BorderSide(color: Colors.blue), ), ), - style: TextStyle(color: Colors.white), + style: TextStyle(color: themeProvider.isDarkMode ? Colors.white : Colors.black), ), SizedBox(height: 16), TextField( controller: messageController, decoration: InputDecoration( labelText: 'Message', - labelStyle: TextStyle(color: Colors.white), + labelStyle: TextStyle( + color: themeProvider.isDarkMode ? Colors.white : Colors.black, + ), enabledBorder: OutlineInputBorder( - borderSide: BorderSide(color: Colors.white), + borderSide: BorderSide( + color: themeProvider.isDarkMode ? Colors.white : Colors.black, + ), ), focusedBorder: OutlineInputBorder( borderSide: BorderSide(color: Colors.blue), ), ), - style: TextStyle(color: Colors.white), + style: TextStyle(color: themeProvider.isDarkMode ? Colors.white : Colors.black), maxLines: 5, ), SizedBox(height: 20), diff --git a/team_b/yappy/lib/help.dart b/team_b/yappy/lib/help.dart index f1e3fb5b..d830c9c6 100644 --- a/team_b/yappy/lib/help.dart +++ b/team_b/yappy/lib/help.dart @@ -1,7 +1,8 @@ import 'package:flutter/material.dart'; import 'package:yappy/tool_bar.dart'; import 'package:yappy/tutorial_page.dart'; - +import 'package:yappy/theme_provider.dart'; +import 'package:provider/provider.dart'; class HelpApp extends StatelessWidget { const HelpApp({super.key}); @@ -20,8 +21,9 @@ class HelpPage extends StatelessWidget { @override Widget build(BuildContext context) { + final themeProvider = Provider.of(context); + return Scaffold( - backgroundColor: const Color.fromARGB(255, 0, 0, 0), appBar: PreferredSize( preferredSize: Size.fromHeight(MediaQuery.of(context).size.height * 0.2), child: ToolBar() @@ -37,8 +39,9 @@ class HelpPage extends StatelessWidget { child: Text( 'Lets Yap about Yappy', style: TextStyle( - color: Colors.white, + color: themeProvider.isDarkMode ? Colors.white: Colors.black, fontSize: 24, + fontWeight: FontWeight.bold, ), ), ), @@ -52,8 +55,9 @@ class HelpPage extends StatelessWidget { Text( 'Welcome to Yappy! If this is your first time and need help with using Yappy, please select the button below.', style: TextStyle( - color: const Color.fromRGBO(255, 255, 255, 1), - fontSize: 12, + color: themeProvider.isDarkMode ? Colors.white: Colors.black, + fontSize: 14, + fontWeight: FontWeight.bold, ), ), SizedBox(height: 20), @@ -72,8 +76,9 @@ class HelpPage extends StatelessWidget { 'Reporting a Problem with Yappy\n' 'If something is not working on Yappy, please follow the instructions below to let us know.\n\n', style: TextStyle( - color: Colors.white, - fontSize: 12, + color: themeProvider.isDarkMode ? Colors.white: Colors.black, + fontSize: 14, + fontWeight: FontWeight.bold, ), ), SizedBox(height: 5), @@ -103,8 +108,9 @@ class HelpPage extends StatelessWidget { Text( '\n\nFeedback from the people who use Yappy has helped us redesign our products, improve our policies and fix technical problems. We really appreciate you taking the time to share your thoughts and suggestions with us.\n', style: TextStyle( - color: Colors.white, - fontSize: 12, + color: themeProvider.isDarkMode ? Colors.white: Colors.black, + fontSize: 16, + fontWeight: FontWeight.bold, ), ), SizedBox(height: 5), diff --git a/team_b/yappy/lib/home_page.dart b/team_b/yappy/lib/home_page.dart index 578dc7d6..e117bdb9 100644 --- a/team_b/yappy/lib/home_page.dart +++ b/team_b/yappy/lib/home_page.dart @@ -90,7 +90,6 @@ class _HomePageState extends State { @override Widget build(BuildContext context) { return Scaffold( - backgroundColor: const Color.fromARGB(255, 0, 0, 0), appBar: PreferredSize( preferredSize: Size.fromHeight(140), child: ToolBar(showHamburger: false), // Using the ToolBar widget @@ -123,7 +122,6 @@ class _HomePageState extends State { ); }, style: ElevatedButton.styleFrom( - backgroundColor: Colors.grey[900], padding: const EdgeInsets.symmetric(vertical: 15), ), child: Text( diff --git a/team_b/yappy/lib/industry_menu.dart b/team_b/yappy/lib/industry_menu.dart index 0217e665..f3bce188 100644 --- a/team_b/yappy/lib/industry_menu.dart +++ b/team_b/yappy/lib/industry_menu.dart @@ -195,6 +195,8 @@ class _IndustryMenuState extends State { // Gets the width and height of the current screen double screenWidth = MediaQuery.of(context).size.width; double screenHeight = MediaQuery.of(context).size.height; + final isDarkMode = Theme.of(context).brightness == Brightness.dark; + return Padding( padding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 16.0), @@ -203,12 +205,12 @@ class _IndustryMenuState extends State { child: Column( children: [ Center( - // Creates the text box above the icons - child: Container( + // Creates the text box above the icons + child: Container( width: screenWidth * .75, decoration: BoxDecoration( borderRadius: BorderRadius.circular(12), - color: const Color.fromARGB(255, 67, 67, 67), + color: isDarkMode ? Color.fromARGB(255, 79, 79, 83): Colors.green, ), padding: EdgeInsets.all(12), child: Center( @@ -232,7 +234,7 @@ class _IndustryMenuState extends State { color: !modelsExist ? Color.fromRGBO(128, 128, 128, 0.5) : (widget.speechState.recordState == RecordState.stop - ? Colors.grey + ? isDarkMode ? Colors.grey : Colors.green : Colors.red)), padding: EdgeInsets.all(5), child: Tooltip( @@ -247,7 +249,7 @@ class _IndustryMenuState extends State { ? Icons.mic : Icons.stop, color: !modelsExist - ? Color.fromRGBO(255, 255, 255, 0.5) + ? isDarkMode ? Colors.grey : Colors.green : Colors.white, size: screenHeight * .05, ), @@ -343,7 +345,7 @@ class _IndustryMenuState extends State { // Creates a industry specific icon based on user input Container( decoration: - BoxDecoration(shape: BoxShape.circle, color: Colors.grey), + BoxDecoration(shape: BoxShape.circle, color: isDarkMode ? Colors.grey : Colors.green), padding: EdgeInsets.all(5), child: IconButton( icon: Icon( @@ -361,7 +363,7 @@ class _IndustryMenuState extends State { // Creates a transcript history button Container( decoration: - BoxDecoration(shape: BoxShape.circle, color: Colors.grey), + BoxDecoration(shape: BoxShape.circle, color: isDarkMode ? Colors.grey : Colors.green), padding: EdgeInsets.all(5), child: IconButton( icon: Icon( @@ -379,7 +381,7 @@ class _IndustryMenuState extends State { // Creates a chatbot button Container( - decoration: BoxDecoration(shape: BoxShape.circle, color: Colors.grey), + decoration: BoxDecoration(shape: BoxShape.circle, color: isDarkMode ? Colors.grey : Colors.green), padding: EdgeInsets.all(5), child: IconButton( icon: Icon( @@ -483,7 +485,7 @@ class _IndustryMenuState extends State { void _showTranscriptsBottomSheet(BuildContext context) async { // Fetch transcripts first List> transcripts = await _fetchTranscripts(); - + final isDarkMode = Theme.of(context).brightness == Brightness.dark; // Check if the context is still valid if (!context.mounted) return; @@ -494,7 +496,7 @@ class _IndustryMenuState extends State { padding: EdgeInsets.all(16.0), decoration: BoxDecoration( borderRadius: BorderRadius.circular(12), - color: const Color.fromARGB(255, 67, 67, 67), + color: isDarkMode ? const Color.fromARGB(255, 67, 67, 67) : Colors.green, ), child: Center( child: Column( @@ -595,6 +597,7 @@ class _IndustryMenuState extends State { void _showTranscriptsHistoryBottomSheet(BuildContext context) async { // Fetch transcripts first List> transcripts = await _fetchTranscripts(); + final isDarkMode = Theme.of(context).brightness == Brightness.dark; // Check if the context is still valid if (!context.mounted) return; @@ -606,7 +609,7 @@ class _IndustryMenuState extends State { padding: EdgeInsets.all(16.0), decoration: BoxDecoration( borderRadius: BorderRadius.circular(12), - color: const Color.fromARGB(255, 67, 67, 67), + color: isDarkMode ? const Color.fromARGB(255, 67, 67, 67) : Colors.green, ), child: Center( child: Column( diff --git a/team_b/yappy/lib/main.dart b/team_b/yappy/lib/main.dart index b19706b7..70d1e011 100644 --- a/team_b/yappy/lib/main.dart +++ b/team_b/yappy/lib/main.dart @@ -5,16 +5,21 @@ import 'package:yappy/services/database_helper.dart'; import 'package:dart_openai/dart_openai.dart'; import 'package:yappy/env.dart'; import './toast_widget.dart'; +import 'package:provider/provider.dart'; +import 'theme_provider.dart'; // Create a global instance of DatabaseHelper final DatabaseHelper dbHelper = DatabaseHelper(); final GlobalKey navigatorKey = GlobalKey(); late SharedPreferences preferences; -void main() async{ +void main() async { WidgetsFlutterBinding.ensureInitialized(); preferences = await SharedPreferences.getInstance(); + // Load the theme preference from SharedPreferences + final isDarkMode = preferences.getBool('toggle_setting') ?? false; + // Env file setup for local development String apiKey = Env.apiKey; if (apiKey.isNotEmpty) { @@ -47,7 +52,13 @@ void main() async{ } }); - runApp(const MyApp()); + // Run the app and initialize ThemeProvider + runApp( + ChangeNotifierProvider( + create: (_) => ThemeProvider()..toggleTheme(isDarkMode), // Initialize with saved theme + child: const MyApp(), + ), + ); } class MyApp extends StatelessWidget { @@ -55,14 +66,15 @@ class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { + final themeProvider = Provider.of(context); + return MaterialApp( navigatorKey: navigatorKey, debugShowCheckedModeBanner: false, title: 'Yappy', - theme: ThemeData( - primarySwatch: Colors.lightGreen, - visualDensity: VisualDensity.adaptivePlatformDensity, - ), + theme: themeProvider.themeData, // Apply theme based on provider + darkTheme: ThemeProvider.darkTheme, // Provide dark theme separately + themeMode: themeProvider.isDarkMode ? ThemeMode.dark : ThemeMode.light, // Set theme mode based on isDarkMode home: HomePage(), builder: (context, child) { // Wrap every screen with ToastWidget @@ -70,4 +82,4 @@ class MyApp extends StatelessWidget { }, ); } -} \ No newline at end of file +} diff --git a/team_b/yappy/lib/mechanic.dart b/team_b/yappy/lib/mechanic.dart index 6029a25c..81d0b3d7 100644 --- a/team_b/yappy/lib/mechanic.dart +++ b/team_b/yappy/lib/mechanic.dart @@ -31,7 +31,6 @@ class MechanicalAidPage extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( - backgroundColor: const Color.fromARGB(255, 0, 0, 0), appBar: PreferredSize( preferredSize: Size.fromHeight(100), child: ToolBar(), diff --git a/team_b/yappy/lib/medical_doctor.dart b/team_b/yappy/lib/medical_doctor.dart index 7ae00cf4..0c035e5b 100644 --- a/team_b/yappy/lib/medical_doctor.dart +++ b/team_b/yappy/lib/medical_doctor.dart @@ -28,7 +28,6 @@ class MedicalDoctorPage extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( - backgroundColor: const Color.fromARGB(255, 0, 0, 0), appBar: PreferredSize( preferredSize: Size.fromHeight(100), child: ToolBar() diff --git a/team_b/yappy/lib/medical_patient.dart b/team_b/yappy/lib/medical_patient.dart index a5b7eaf5..52b4a970 100644 --- a/team_b/yappy/lib/medical_patient.dart +++ b/team_b/yappy/lib/medical_patient.dart @@ -28,7 +28,6 @@ class MedicalPatientPage extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( - backgroundColor: const Color.fromARGB(255, 0, 0, 0), appBar: PreferredSize( preferredSize: Size.fromHeight(100), child: ToolBar() diff --git a/team_b/yappy/lib/restaurant.dart b/team_b/yappy/lib/restaurant.dart index 62e3f215..20df3c55 100644 --- a/team_b/yappy/lib/restaurant.dart +++ b/team_b/yappy/lib/restaurant.dart @@ -15,7 +15,6 @@ class RestaurantPage extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( - backgroundColor: const Color.fromARGB(255, 0, 0, 0), appBar: PreferredSize( preferredSize: Size.fromHeight(100), child: ToolBar(), diff --git a/team_b/yappy/lib/search_bar_widget.dart b/team_b/yappy/lib/search_bar_widget.dart index 20c8b3d1..822fe9d0 100644 --- a/team_b/yappy/lib/search_bar_widget.dart +++ b/team_b/yappy/lib/search_bar_widget.dart @@ -41,7 +41,6 @@ class _SearchBarWidgetState extends State { @override Widget build(BuildContext context) { return Container( - color: const Color.fromARGB(255, 0, 0, 0), child: Padding( padding: const EdgeInsets.only(left: 4.0, right: 4.0), child: SearchAnchor( diff --git a/team_b/yappy/lib/settings_page.dart b/team_b/yappy/lib/settings_page.dart index 54835e6b..81934068 100644 --- a/team_b/yappy/lib/settings_page.dart +++ b/team_b/yappy/lib/settings_page.dart @@ -1,8 +1,10 @@ import 'package:dart_openai/dart_openai.dart'; import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; import 'package:shared_preferences/shared_preferences.dart'; import './services/model_manager.dart'; import 'package:yappy/main.dart'; +import 'package:yappy/theme_provider.dart'; class SettingsPage extends StatefulWidget { const SettingsPage({super.key}); @@ -50,6 +52,8 @@ class _SettingsPageState extends State { @override Widget build(BuildContext context) { + final themeProvider = Provider.of(context); + return Scaffold( appBar: AppBar( title: Text('Settings'), @@ -122,7 +126,15 @@ class _SettingsPageState extends State { ); }, ), - + SwitchListTile( + title: const Text('Dark Mode'), + subtitle: const Text('Toggle Dark Mode on or off'), + value: themeProvider.isDarkMode, + onChanged: (value) { + themeProvider.toggleTheme(value); + }, + ), + // Divider to separate original and new settings const Divider(), @@ -250,4 +262,5 @@ class _SettingsPageState extends State { ); } } + } \ No newline at end of file diff --git a/team_b/yappy/lib/theme_provider.dart b/team_b/yappy/lib/theme_provider.dart new file mode 100644 index 00000000..d6f1e0d0 --- /dev/null +++ b/team_b/yappy/lib/theme_provider.dart @@ -0,0 +1,61 @@ +import 'package:flutter/material.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +class ThemeProvider with ChangeNotifier { + bool _isDarkMode = false; + + bool get isDarkMode => _isDarkMode; + + ThemeProvider() { + _loadThemePreference(); + } + + Future _loadThemePreference() async { + final prefs = await SharedPreferences.getInstance(); + _isDarkMode = prefs.getBool('toggle_setting') ?? false; + notifyListeners(); + } + + Future toggleTheme(bool value) async { + _isDarkMode = value; + final prefs = await SharedPreferences.getInstance(); + await prefs.setBool('toggle_setting', value); + notifyListeners(); + } + + ThemeData get themeData { + return _isDarkMode ? darkTheme : lightTheme; + } + + static final ThemeData lightTheme = ThemeData( + brightness: Brightness.light, + primarySwatch: Colors.lightGreen, + visualDensity: VisualDensity.adaptivePlatformDensity, + elevatedButtonTheme: ElevatedButtonThemeData( + style: ElevatedButton.styleFrom( + backgroundColor: Colors.green, + foregroundColor: Colors.black, + ), + ), + scaffoldBackgroundColor: Colors.white, + iconTheme: const IconThemeData( + color: Colors.black, + ), + ); + + static final ThemeData darkTheme = ThemeData( + brightness: Brightness.dark, + primarySwatch: Colors.grey, + visualDensity: VisualDensity.adaptivePlatformDensity, + elevatedButtonTheme: ElevatedButtonThemeData( + style: ElevatedButton.styleFrom( + backgroundColor: Color.fromARGB(255, 79, 79, 83), + foregroundColor: Colors.white, + ), + ), + scaffoldBackgroundColor: Colors.black, + iconTheme: const IconThemeData( + color: Colors.white, + ), + ); +} diff --git a/team_b/yappy/lib/tool_bar.dart b/team_b/yappy/lib/tool_bar.dart index 63ceb8b1..26f25568 100644 --- a/team_b/yappy/lib/tool_bar.dart +++ b/team_b/yappy/lib/tool_bar.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; import 'package:yappy/home_page.dart'; import 'package:yappy/restaurant.dart'; import 'package:yappy/contact_page.dart'; @@ -7,6 +8,7 @@ import 'package:yappy/medical_patient.dart'; import 'package:yappy/medical_doctor.dart'; import 'package:yappy/mechanic.dart'; import 'package:yappy/settings_page.dart'; +import 'package:yappy/theme_provider.dart'; // Defines a reusable Hamburger Menu Widget (AppBar + Drawer) class ToolBar extends StatelessWidget { @@ -18,92 +20,91 @@ class ToolBar extends StatelessWidget { Widget build(BuildContext context) { double screenWidth = MediaQuery.of(context).size.width; double screenHeight = MediaQuery.of(context).size.height; + final themeProvider = Provider.of(context); return AppBar( - // Creates the hamburger icon for the menu - backgroundColor: Colors.black, - leading: showHamburger - ? Builder( - builder: (context) { - return IconButton( - icon: const Icon(Icons.menu, color: Colors.white), - onPressed: () { - Scaffold.of(context).openDrawer(); - }, - ); - }, - ): SizedBox(width: screenHeight * 0.08), - toolbarHeight: screenHeight * 0.11, - // Contains the Yappy! icon - title: Center( - child: CircleAvatar( - backgroundColor: const Color.fromARGB(255, 0, 0, 0), - radius: screenWidth * 0.2, - child: Image.asset( - 'assets/icon/app_icon.png', - width: screenWidth * 0.22, - height: screenWidth * 0.22, - ), + // Background color based on the theme + backgroundColor: themeProvider.isDarkMode ? Colors.black : Colors.white, + leading: showHamburger + ? Builder( + builder: (context) { + return IconButton( + icon: Icon(Icons.menu, color: themeProvider.isDarkMode ? Colors.white : Colors.black), + onPressed: () { + Scaffold.of(context).openDrawer(); + }, + ); + }, + ) : SizedBox(width: screenHeight * 0.08), + toolbarHeight: screenHeight * 0.11, + title: Center( + child: CircleAvatar( + backgroundColor: themeProvider.isDarkMode ? Colors.black : Colors.white, + radius: screenWidth * 0.2, + child: Image.asset( + 'assets/icon/app_icon.png', + width: screenWidth * 0.22, + height: screenWidth * 0.22, ), ), - actions: [ - // Contains the information button - IconButton( - icon: const Icon(Icons.info, color: Colors.white), - onPressed: () { - showDialog( - context: context, - builder: (BuildContext context) { - // Creates a pop up when the button is pressed - return AlertDialog( - title: const Text('Information'), - content: const Text('Yappy Terms & Conditions\n\n''By using Yappy,' - 'you agree to Use the app responsibly and comply ' - 'with all applicable laws. Respect user privacy ' - 'and refrain from harmful or abusive behavior.\n\n' - 'Understand that Yappy is not liable for ' - 'any misuse or legal consequences arising from its use. ' - 'We may update these terms as needed.\n\n' - 'Continued use of Yappy means acceptance of any changes..'), - actions: [ - TextButton( - onPressed: () { - Navigator.of(context).pop(); - }, - child: const Text('OK'), - ), - ], - ); - }, - ); - }, - ), - ], - ); + ), + actions: [ + // Information button with dynamic color + IconButton( + icon: Icon(Icons.info, color: themeProvider.isDarkMode ? Colors.white : Colors.green), + onPressed: () { + showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + title: Text('Information', style: TextStyle(color: themeProvider.isDarkMode ? Colors.white : Colors.black)), + content: Text( + 'Yappy Terms & Conditions\n\nBy using Yappy, you agree to use the app responsibly and comply with all applicable laws. Respect user privacy and refrain from harmful or abusive behavior.\n\nUnderstand that Yappy is not liable for any misuse or legal consequences arising from its use. We may update these terms as needed.\n\nContinued use of Yappy means acceptance of any changes.', + style: TextStyle(color: themeProvider.isDarkMode ? Colors.white : Colors.black), + ), + actions: [ + TextButton( + onPressed: () { + Navigator.of(context).pop(); + }, + child: Text('OK', style: TextStyle(color: themeProvider.isDarkMode ? Colors.white : Colors.black)), + ), + ], + ); + }, + ); + }, + ), + ], + ); } } - // creates the hamburger menu + +// Creates the hamburger menu class HamburgerDrawer extends StatelessWidget { const HamburgerDrawer({super.key}); @override Widget build(BuildContext context) { + final themeProvider = Provider.of(context); + double screenWidth = MediaQuery.of(context).size.width; return SafeArea( child: Drawer( width: screenWidth * .45, - backgroundColor: const Color.fromARGB(255, 54, 54, 54), + // Background color based on theme + backgroundColor: themeProvider.isDarkMode ? Color.fromARGB(255, 79, 79, 83) : Colors.green, child: ListView( padding: EdgeInsets.zero, children: [ - _buildDrawerItem('Home', context, HomePage()), - _buildDrawerItem('Restaurant', context, RestaurantPage()), - _buildDrawerItem('Vehicle Maintenance', context, MechanicalAidPage()), - _buildDrawerItem('Medical Doctor', context, MedicalDoctorPage()), - _buildDrawerItem('Medical Patient', context, MedicalPatientPage()), - _buildDrawerItem('Help', context, HelpPage()), - _buildDrawerItem('Contact', context, ContactPage()), - _buildDrawerItem('Settings', context, SettingsPage()), + _buildDrawerItem('Home', context, HomePage(), themeProvider), + _buildDrawerItem('Restaurant', context, RestaurantPage(), themeProvider), + _buildDrawerItem('Vehicle Maintenance', context, MechanicalAidPage(), themeProvider), + _buildDrawerItem('Medical Doctor', context, MedicalDoctorPage(), themeProvider), + _buildDrawerItem('Medical Patient', context, MedicalPatientPage(), themeProvider), + _buildDrawerItem('Help', context, HelpPage(), themeProvider), + _buildDrawerItem('Contact', context, ContactPage(), themeProvider), + _buildDrawerItem('Settings', context, SettingsPage(), themeProvider), ], ), ), @@ -111,19 +112,21 @@ class HamburgerDrawer extends StatelessWidget { } // Creates the individual drawer items for the hamburger menu - Widget _buildDrawerItem(String title, BuildContext context, Widget page) { + Widget _buildDrawerItem(String title, BuildContext context, Widget page, ThemeProvider themeProvider) { return ListTile( contentPadding: EdgeInsets.symmetric(vertical: 8.0, horizontal: 16.0), - textColor: Colors.white, - tileColor: const Color.fromARGB(255, 54, 54, 54), - title: Text(title), + textColor: themeProvider.isDarkMode ? Colors.white : Colors.black, // Text color based on theme + tileColor: themeProvider.isDarkMode ? Color.fromARGB(255, 54, 54, 54) : Colors.green.shade200, + title: Text( + title, + style: TextStyle(color: themeProvider.isDarkMode ? Colors.white : Colors.black), // Text color based on theme + ), onTap: () { Navigator.push( context, - // Navigates to the page once the button is clicked MaterialPageRoute(builder: (context) => page), ); }, ); } -} \ No newline at end of file +} diff --git a/team_b/yappy/lib/transcription_box.dart b/team_b/yappy/lib/transcription_box.dart index 8076f375..e2a59642 100644 --- a/team_b/yappy/lib/transcription_box.dart +++ b/team_b/yappy/lib/transcription_box.dart @@ -1,4 +1,6 @@ import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:yappy/theme_provider.dart'; class TranscriptionBox extends StatefulWidget { final TextEditingController controller; @@ -40,6 +42,7 @@ class TranscriptionBoxState extends State { @override Widget build(BuildContext context) { + final themeProvider = Provider.of(context); double screenHeight = MediaQuery.of(context).size.height; return SizedBox( @@ -47,7 +50,7 @@ class TranscriptionBoxState extends State { child: Container( decoration: BoxDecoration( borderRadius: BorderRadius.circular(8.0), - color: const Color.fromARGB(255, 67, 67, 67), + color: themeProvider.isDarkMode ? const Color.fromARGB(255, 67, 67, 67): Colors.green, ), child: Scrollbar( controller: _scrollController, @@ -59,8 +62,8 @@ class TranscriptionBoxState extends State { maxLines: null, readOnly: true, decoration: InputDecoration( - hintText: "Transcription will appear here...", - hintStyle: TextStyle(color: Colors.white), + hintText: "", + hintStyle: TextStyle(color:Colors.white), border: InputBorder.none, contentPadding: EdgeInsets.all(10), ), diff --git a/team_b/yappy/pubspec.yaml b/team_b/yappy/pubspec.yaml index 4d1359e0..de0f428a 100644 --- a/team_b/yappy/pubspec.yaml +++ b/team_b/yappy/pubspec.yaml @@ -50,6 +50,7 @@ dependencies: file_picker: ^9.0.2 flutter_audio_waveforms: ^1.2.1+8 dart_openai: ^5.1.0 + provider: ^6.1.4 dev_dependencies: flutter_test: From 3a3cf4a83a71d9e2207be87641d4957f719b1cc4 Mon Sep 17 00:00:00 2001 From: z4sythe Date: Thu, 27 Mar 2025 20:00:15 -0400 Subject: [PATCH 8/8] Flutter analyze fixes --- team_b/yappy/lib/industry_menu.dart | 2 ++ team_b/yappy/lib/search_bar_widget.dart | 46 ++++++++++++------------- 2 files changed, 24 insertions(+), 24 deletions(-) diff --git a/team_b/yappy/lib/industry_menu.dart b/team_b/yappy/lib/industry_menu.dart index f3bce188..bb0534ed 100644 --- a/team_b/yappy/lib/industry_menu.dart +++ b/team_b/yappy/lib/industry_menu.dart @@ -485,6 +485,7 @@ class _IndustryMenuState extends State { void _showTranscriptsBottomSheet(BuildContext context) async { // Fetch transcripts first List> transcripts = await _fetchTranscripts(); + if (!context.mounted) return; final isDarkMode = Theme.of(context).brightness == Brightness.dark; // Check if the context is still valid if (!context.mounted) return; @@ -597,6 +598,7 @@ class _IndustryMenuState extends State { void _showTranscriptsHistoryBottomSheet(BuildContext context) async { // Fetch transcripts first List> transcripts = await _fetchTranscripts(); + if (!context.mounted) return; final isDarkMode = Theme.of(context).brightness == Brightness.dark; // Check if the context is still valid diff --git a/team_b/yappy/lib/search_bar_widget.dart b/team_b/yappy/lib/search_bar_widget.dart index 822fe9d0..4df852dd 100644 --- a/team_b/yappy/lib/search_bar_widget.dart +++ b/team_b/yappy/lib/search_bar_widget.dart @@ -40,30 +40,28 @@ class _SearchBarWidgetState extends State { @override Widget build(BuildContext context) { - return Container( - child: Padding( - padding: const EdgeInsets.only(left: 4.0, right: 4.0), - child: SearchAnchor( - searchController: _searchController, - suggestionsBuilder: (BuildContext context, SearchController controller) async { - return _fetchSuggestions(controller.text, widget.industry); - }, - builder: (BuildContext context, SearchController controller) { - return SearchBar( - controller: _searchController, - padding: const WidgetStatePropertyAll( - EdgeInsets.symmetric(horizontal: 2.0), - ), - onTap: () { - controller.openView(); - }, - onChanged: (_) { - controller.openView(); - }, - leading: const Icon(Icons.search), - ); - }, - ), + return Padding( + padding: const EdgeInsets.only(left: 4.0, right: 4.0), + child: SearchAnchor( + searchController: _searchController, + suggestionsBuilder: (BuildContext context, SearchController controller) async { + return _fetchSuggestions(controller.text, widget.industry); + }, + builder: (BuildContext context, SearchController controller) { + return SearchBar( + controller: _searchController, + padding: const WidgetStatePropertyAll( + EdgeInsets.symmetric(horizontal: 2.0), + ), + onTap: () { + controller.openView(); + }, + onChanged: (_) { + controller.openView(); + }, + leading: const Icon(Icons.search), + ); + }, ), ); }