ciaochris commited on
Commit
5d6886b
·
verified ·
1 Parent(s): 080e56f

Update rhythma.py

Browse files
Files changed (1) hide show
  1. rhythma.py +256 -1
rhythma.py CHANGED
@@ -407,5 +407,260 @@ class RhythmaSymphAICore:
407
  print("ℹ️ SentenceTransformer not installed. Using basic text matching.")
408
 
409
 
 
410
  def detect_emotion_with_groq(self, input_text):
411
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
407
  print("ℹ️ SentenceTransformer not installed. Using basic text matching.")
408
 
409
 
410
+ # Line 343:
411
  def detect_emotion_with_groq(self, input_text):
412
+ """Use Groq LLM to detect emotion/intention in text"""
413
+ if not self.use_groq or not self.groq_client:
414
+ print("ℹ️ Groq not available or not initialized for emotion detection.")
415
+ return None # Indicate Groq wasn't used
416
+
417
+ # Refined prompt for better classification into our categories
418
+ available_states = ", ".join(self.emotional_states)
419
+ prompt = f"""Analyze the user's feeling described below.
420
+ Identify the single MOST prominent emotional state or intention from the following list:
421
+ {available_states}
422
+
423
+ Focus on the core feeling expressed. Respond with ONLY the chosen state/intention from the list.
424
+
425
+ User's feeling: "{input_text}"
426
+ State/Intention:"""
427
+
428
+ try:
429
+ print(f"ℹ️ Querying Groq for emotion analysis...")
430
+ chat_completion = self.groq_client.chat.completions.create(
431
+ messages=[{"role": "user", "content": prompt}],
432
+ model="llama3-70b-8192", # Specify a capable model available on Groq
433
+ max_tokens=15, # Allow slightly more tokens for flexibility
434
+ temperature=0.2, # Lower temperature for more deterministic classification
435
+ stop=["\n"] # Stop generation after the first line
436
+ )
437
+
438
+ detected_emotion = chat_completion.choices[0].message.content.strip().lower()
439
+ print(f"✅ Groq detected: {detected_emotion}")
440
+
441
+ # Validate the detected emotion against our list
442
+ if detected_emotion in self.emotional_states:
443
+ return detected_emotion
444
+ else:
445
+ print(f"⚠️ Groq returned '{detected_emotion}', not in known states. Attempting fallback match.")
446
+ # Fallback: If LLM returns something unexpected, find the closest match in our list
447
+ return self.get_closest_emotional_state(detected_emotion) # Use fallback on unexpected LLM output
448
+
449
+ except Exception as e:
450
+ print(f"❌ Error using Groq for emotion detection: {str(e)}")
451
+ traceback.print_exc()
452
+ return None # Indicate error or inability to use Groq
453
+
454
+
455
+ def get_closest_emotional_state(self, input_text):
456
+ """Map input text to the closest emotional state using available methods."""
457
+ if not input_text:
458
+ return "neutral" # Default if no text
459
+
460
+ input_text_lower = input_text.lower()
461
+
462
+ # 1. Try simple keyword matching first (fastest)
463
+ for state in self.emotional_states:
464
+ if state in input_text_lower.split(): # Match whole words if possible
465
+ print(f"ℹ️ Matched keyword: {state}")
466
+ return state
467
+ # Simple substring check as backup
468
+ if state in input_text_lower:
469
+ print(f"ℹ️ Matched substring: {state}")
470
+ return state
471
+
472
+
473
+ # 2. If Sentence Transformer is available, use semantic similarity
474
+ if self.embedding_model and self.emotional_embeddings:
475
+ try:
476
+ print("ℹ️ Using Sentence Transformer for semantic emotion match.")
477
+ input_embedding = self.embedding_model.encode([input_text])[0] # Get 1D array
478
+ # Calculate cosine similarities
479
+ similarities = {
480
+ state: cosine_similarity(input_embedding.reshape(1, -1), embedding.reshape(1, -1))[0][0]
481
+ for state, embedding in self.emotional_embeddings.items()
482
+ }
483
+ # Find the state with the highest similarity
484
+ best_match = max(similarities, key=similarities.get)
485
+ print(f"✅ Semantic match: {best_match} (Similarity: {similarities[best_match]:.2f})")
486
+ return best_match
487
+ except Exception as e:
488
+ print(f"⚠️ Error during semantic matching: {e}. Falling back.")
489
+ traceback.print_exc()
490
+
491
+ # 3. Default fallback if no match found
492
+ print("ℹ️ No clear emotion match found, defaulting to 'neutral'.")
493
+ return "neutral"
494
+
495
+
496
+ def get_closest_rhythm_pattern(self, input_text=None, emotional_state=None):
497
+ """Map input text or emotional state to the closest rhythm pattern."""
498
+
499
+ # 1. Direct mapping from emotional state (prioritized if state is known)
500
+ if emotional_state:
501
+ # Refined mapping based on typical energy levels/needs
502
+ mapping = {
503
+ "anxious": "calm", # Needs calming
504
+ "stressed": "relaxed", # Needs relaxation
505
+ "calm": "calm",
506
+ "sad": "relaxed", # Gentle support
507
+ "angry": "active", # Needs release/energy shift
508
+ "fearful": "calm", # Needs safety/grounding
509
+ "confused": "focused", # Needs clarity
510
+ "happy": "active", # Can match higher energy
511
+ "neutral": "calm",
512
+ "focused": "focused", # Align with intention
513
+ "relaxed": "relaxed", # Align with intention
514
+ "active": "active", # Align with intention
515
+ }
516
+ pattern = mapping.get(emotional_state, "calm") # Default to calm if state unknown
517
+ print(f"ℹ️ Rhythm pattern from state '{emotional_state}': {pattern}")
518
+ return pattern
519
+
520
+ # 2. If no emotional state, try matching input text semantically (if available)
521
+ if input_text and self.embedding_model and self.rhythm_embeddings:
522
+ try:
523
+ print("ℹ️ Using Sentence Transformer for semantic rhythm match.")
524
+ input_embedding = self.embedding_model.encode([input_text])[0]
525
+ similarities = {
526
+ pattern: cosine_similarity(input_embedding.reshape(1, -1), embedding.reshape(1, -1))[0][0]
527
+ for pattern, embedding in self.rhythm_embeddings.items()
528
+ }
529
+ best_match = max(similarities, key=similarities.get)
530
+ print(f"✅ Semantic rhythm match: {best_match} (Similarity: {similarities[best_match]:.2f})")
531
+ return best_match
532
+ except Exception as e:
533
+ print(f"⚠️ Error during semantic rhythm matching: {e}. Falling back.")
534
+ traceback.print_exc()
535
+
536
+ # 3. Default fallback
537
+ print("ℹ️ Defaulting rhythm pattern to 'calm'.")
538
+ return "calm"
539
+
540
+
541
+ def transcribe_audio(self, audio_path):
542
+ """Transcribe audio using Groq Whisper if available"""
543
+ if not self.use_groq or not self.groq_client:
544
+ print("ℹ️ Groq not available for transcription.")
545
+ return None, "Transcription disabled: Groq client not available or API key missing."
546
+
547
+ if not audio_path or not os.path.exists(audio_path):
548
+ return None, "Transcription failed: Audio file path is invalid or missing."
549
+
550
+ try:
551
+ print(f"ℹ️ Transcribing audio file: {audio_path}")
552
+ with open(audio_path, "rb") as audio_file:
553
+ # Use whisper-large-v3 for potentially better accuracy
554
+ transcription_response = self.groq_client.audio.transcriptions.create(
555
+ file=(os.path.basename(audio_path), audio_file.read()),
556
+ model="whisper-large-v3", # Using v3
557
+ # response_format="verbose_json", # Get more details if needed
558
+ response_format="json", # Simpler format
559
+ )
560
+
561
+ transcribed_text = transcription_response.text
562
+ print(f"✅ Groq transcription successful: '{transcribed_text}'")
563
+ return transcribed_text, None # Return text and no error
564
+
565
+ except Exception as e:
566
+ error_message = f"Error during Groq transcription: {str(e)}"
567
+ print(f"❌ {error_message}")
568
+ traceback.print_exc()
569
+ return None, error_message # Return None and the error message
570
+
571
+ # --- THIS IS THE FUNCTION STARTING AT LINE 410 ---
572
+ def analyze_input(self, input_text=None, audio_path=None):
573
+ """
574
+ Analyze input text and/or audio path to determine emotional state and rhythm pattern.
575
+ **Ensures a dictionary is always returned.**
576
+ """
577
+ # ---> Line 411: Ensure this block is indented <---
578
+ analysis_result = {
579
+ "emotional_state": "neutral", # Default values
580
+ "rhythm_pattern": "calm", # Default values
581
+ "transcription": "",
582
+ "error": None
583
+ }
584
+ text_to_analyze = None
585
+ transcription_error = None
586
+
587
+ # ---> All lines below here inside the function must also be indented <---
588
+ print("-" * 20) # Separator for logs
589
+ print(f"ℹ️ SymphAI Core analyzing input: Text='{input_text}', Audio='{audio_path}'")
590
+
591
+ try:
592
+ # --- Step 1: Handle Audio Input (if provided and Groq available) ---
593
+ if audio_path and self.use_groq:
594
+ transcribed_text, transcription_error = self.transcribe_audio(audio_path)
595
+ if transcription_error:
596
+ print(f"⚠️ Transcription failed: {transcription_error}")
597
+ # Store error but potentially continue with text input if available
598
+ analysis_result["error"] = transcription_error
599
+ analysis_result["transcription"] = f"[Transcription Error: {transcription_error}]"
600
+ elif transcribed_text:
601
+ analysis_result["transcription"] = transcribed_text
602
+ text_to_analyze = transcribed_text # Prioritize transcribed text
603
+ print(f"ℹ️ Using transcribed text for analysis: '{text_to_analyze}'")
604
+
605
+ # --- Step 2: Determine Text for Analysis ---
606
+ if not text_to_analyze and input_text:
607
+ text_to_analyze = input_text # Use input_text if no successful transcription
608
+ print(f"ℹ️ Using provided text for analysis: '{text_to_analyze}'")
609
+ elif not text_to_analyze:
610
+ print("ℹ️ No text input or successful transcription available for analysis.")
611
+ # Keep default neutral/calm state
612
+
613
+ # --- Step 3: Detect Emotional State (if text available) ---
614
+ detected_emotion = None
615
+ if text_to_analyze:
616
+ if self.use_groq:
617
+ detected_emotion = self.detect_emotion_with_groq(text_to_analyze)
618
+ if detected_emotion:
619
+ analysis_result["emotional_state"] = detected_emotion
620
+ else:
621
+ # Groq failed or didn't run, try fallback
622
+ print("ℹ️ Groq emotion detection failed or skipped, trying fallback.")
623
+ analysis_result["emotional_state"] = self.get_closest_emotional_state(text_to_analyze)
624
+ else:
625
+ # Groq not used, directly use fallback
626
+ analysis_result["emotional_state"] = self.get_closest_emotional_state(text_to_analyze)
627
+ else:
628
+ # No text to analyze, stick with default "neutral"
629
+ analysis_result["emotional_state"] = "neutral"
630
+
631
+
632
+ # --- Step 4: Determine Rhythm Pattern ---
633
+ # Use the determined emotional state primarily, fallback to text if needed
634
+ current_emotion = analysis_result["emotional_state"]
635
+ analysis_result["rhythm_pattern"] = self.get_closest_rhythm_pattern(
636
+ input_text=text_to_analyze, # Pass text for potential semantic match if emotion is neutral/unclear
637
+ emotional_state=current_emotion
638
+ )
639
+
640
+ # Clean up error field if no actual error occurred during main analysis
641
+ if analysis_result["error"] is None and transcription_error:
642
+ # If transcription failed but text analysis succeeded, maybe clear the error?
643
+ # Decide if transcription error should persist if text analysis works.
644
+ # Let's keep it for now to inform the user.
645
+ pass
646
+ elif analysis_result["error"] is None:
647
+ # analysis_result.pop("error", None) # Alternative way to remove if None
648
+ del analysis_result["error"] # Remove error key if None
649
+
650
+
651
+ except Exception as e:
652
+ # --- Catch-all for unexpected errors during analysis ---
653
+ error_msg = f"Unexpected error during input analysis: {str(e)}"
654
+ print(f"❌ {error_msg}")
655
+ traceback.print_exc()
656
+ analysis_result = {
657
+ "emotional_state": "neutral", # Reset to defaults on error
658
+ "rhythm_pattern": "calm",
659
+ "transcription": analysis_result.get("transcription", ""), # Keep transcription if available
660
+ "error": error_msg
661
+ }
662
+
663
+ # ---> Ensure these lines are indented correctly at the function level <---
664
+ print(f"✅ SymphAI Core analysis complete. Result: {analysis_result}")
665
+ print("-" * 20) # Separator for logs
666
+ return analysis_result # GUARANTEED TO BE A DICTIONARY