Spaces:
Running
Running
Update rhythma.py
Browse files- 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
|