Spaces:
Running
Running
| import logging | |
| import os | |
| from dataclasses import asdict, dataclass | |
| import numpy as np | |
| from rhythma_engine import RhythmaModulationEngine | |
| try: | |
| from groq import Groq | |
| GROQ_AVAILABLE = True | |
| except ImportError: | |
| Groq = None | |
| GROQ_AVAILABLE = False | |
| LOGGER = logging.getLogger(__name__) | |
| class AnalysisResult: | |
| emotional_state: str = "neutral" | |
| rhythm_pattern: str = "calm" | |
| transcription: str = "" | |
| session_profile: dict | None = None | |
| error: str | None = None | |
| def to_dict(self): | |
| return asdict(self) | |
| class SessionProfile: | |
| key: str | |
| title: str | |
| emotional_tone: str | |
| tone_center: float | |
| pattern: str | |
| modulation_type: str | |
| guidance: str | |
| reflection: str | |
| duration_hint: int | |
| brightness: float | |
| density: float | |
| shimmer: float | |
| breath_rate: float | |
| def to_dict(self): | |
| return asdict(self) | |
| def _cosine_similarity(left, right): | |
| denominator = np.linalg.norm(left) * np.linalg.norm(right) | |
| if denominator == 0: | |
| return -1.0 | |
| return float(np.dot(left, right) / denominator) | |
| SESSION_PRESETS = { | |
| "anxious": SessionProfile( | |
| key="anxious", | |
| title="Grounding Tide", | |
| emotional_tone="Settling and steady", | |
| tone_center=396.0, | |
| pattern="calm", | |
| modulation_type="sine", | |
| guidance="Let your breath fall behind the pulse until the session feels steady.", | |
| reflection="This session favors stability over intensity.", | |
| duration_hint=15, | |
| brightness=0.25, | |
| density=0.45, | |
| shimmer=0.12, | |
| breath_rate=0.08, | |
| ), | |
| "stressed": SessionProfile( | |
| key="stressed", | |
| title="Soft Landing", | |
| emotional_tone="Unwinding and spacious", | |
| tone_center=417.0, | |
| pattern="relaxed", | |
| modulation_type="sine", | |
| guidance="Let the longer exhale soften the edges of the session.", | |
| reflection="This session eases pressure by widening the pulse.", | |
| duration_hint=18, | |
| brightness=0.22, | |
| density=0.38, | |
| shimmer=0.1, | |
| breath_rate=0.07, | |
| ), | |
| "calm": SessionProfile( | |
| key="calm", | |
| title="Quiet Harbor", | |
| emotional_tone="Easeful and settled", | |
| tone_center=432.0, | |
| pattern="calm", | |
| modulation_type="sine", | |
| guidance="Rest inside the repeating tone until it feels effortless.", | |
| reflection="This session keeps motion light to support an even mood.", | |
| duration_hint=15, | |
| brightness=0.32, | |
| density=0.28, | |
| shimmer=0.11, | |
| breath_rate=0.09, | |
| ), | |
| "sad": SessionProfile( | |
| key="sad", | |
| title="Low Ember", | |
| emotional_tone="Tender and reflective", | |
| tone_center=341.3, | |
| pattern="relaxed", | |
| modulation_type="sine", | |
| guidance="Allow the lower tone to hold the feeling without forcing it to lift.", | |
| reflection="This session gives weight and warmth to slower emotion.", | |
| duration_hint=16, | |
| brightness=0.18, | |
| density=0.33, | |
| shimmer=0.08, | |
| breath_rate=0.07, | |
| ), | |
| "angry": SessionProfile( | |
| key="angry", | |
| title="Ember Release", | |
| emotional_tone="Directed and discharging", | |
| tone_center=528.0, | |
| pattern="active", | |
| modulation_type="pulse", | |
| guidance="Track the sharper pulse until it turns from force into direction.", | |
| reflection="This session channels intensity into movement rather than compression.", | |
| duration_hint=12, | |
| brightness=0.5, | |
| density=0.62, | |
| shimmer=0.16, | |
| breath_rate=0.14, | |
| ), | |
| "fearful": SessionProfile( | |
| key="fearful", | |
| title="Shelter Light", | |
| emotional_tone="Protected and steadying", | |
| tone_center=384.0, | |
| pattern="calm", | |
| modulation_type="sine", | |
| guidance="Stay with the nearest tone and let it make the room feel smaller and safer.", | |
| reflection="This session reduces motion so attention can settle close to the body.", | |
| duration_hint=14, | |
| brightness=0.24, | |
| density=0.31, | |
| shimmer=0.09, | |
| breath_rate=0.08, | |
| ), | |
| "confused": SessionProfile( | |
| key="confused", | |
| title="North Star", | |
| emotional_tone="Clarifying and composed", | |
| tone_center=480.0, | |
| pattern="focused", | |
| modulation_type="sine", | |
| guidance="Follow one repeating detail until the rest of the field begins to organize.", | |
| reflection="This session simplifies the soundstage to support orientation.", | |
| duration_hint=14, | |
| brightness=0.34, | |
| density=0.3, | |
| shimmer=0.13, | |
| breath_rate=0.1, | |
| ), | |
| "happy": SessionProfile( | |
| key="happy", | |
| title="Bright Current", | |
| emotional_tone="Open and buoyant", | |
| tone_center=576.0, | |
| pattern="active", | |
| modulation_type="pulse", | |
| guidance="Enjoy the lift in the rhythm without pushing it faster.", | |
| reflection="This session keeps energy lively while protecting headroom.", | |
| duration_hint=12, | |
| brightness=0.56, | |
| density=0.4, | |
| shimmer=0.24, | |
| breath_rate=0.15, | |
| ), | |
| "focused": SessionProfile( | |
| key="focused", | |
| title="Clear Horizon", | |
| emotional_tone="Attentive and composed", | |
| tone_center=512.0, | |
| pattern="focused", | |
| modulation_type="sine", | |
| guidance="Stay with one thought and let the pulse keep the edges quiet.", | |
| reflection="This session narrows motion to support sustained attention.", | |
| duration_hint=20, | |
| brightness=0.4, | |
| density=0.35, | |
| shimmer=0.18, | |
| breath_rate=0.12, | |
| ), | |
| "relaxed": SessionProfile( | |
| key="relaxed", | |
| title="Open Meadow", | |
| emotional_tone="Loose and restorative", | |
| tone_center=444.0, | |
| pattern="relaxed", | |
| modulation_type="sine", | |
| guidance="Let the slow sway in the session keep your attention unforced.", | |
| reflection="This session favors softness and lingering resonance.", | |
| duration_hint=18, | |
| brightness=0.28, | |
| density=0.26, | |
| shimmer=0.12, | |
| breath_rate=0.08, | |
| ), | |
| "active": SessionProfile( | |
| key="active", | |
| title="Kinetic Bloom", | |
| emotional_tone="Motivated and rhythmic", | |
| tone_center=648.0, | |
| pattern="active", | |
| modulation_type="pulse", | |
| guidance="Let the pulse carry forward motion without turning rushed.", | |
| reflection="This session keeps energy articulated and bright.", | |
| duration_hint=10, | |
| brightness=0.6, | |
| density=0.48, | |
| shimmer=0.2, | |
| breath_rate=0.16, | |
| ), | |
| "neutral": SessionProfile( | |
| key="neutral", | |
| title="Still Current", | |
| emotional_tone="Balanced and open", | |
| tone_center=432.0, | |
| pattern="calm", | |
| modulation_type="sine", | |
| guidance="Listen for the simplest pulse and let it set the pace.", | |
| reflection="This session leaves space for your attention to settle naturally.", | |
| duration_hint=12, | |
| brightness=0.3, | |
| density=0.3, | |
| shimmer=0.1, | |
| breath_rate=0.1, | |
| ), | |
| } | |
| class RhythmaSymphAICore: | |
| """ | |
| Interprets text and audio input to determine emotional state and rhythm pattern. | |
| """ | |
| def __init__(self, use_groq=True, use_embeddings=True): | |
| self.emotional_states = [ | |
| "anxious", | |
| "stressed", | |
| "calm", | |
| "sad", | |
| "angry", | |
| "fearful", | |
| "confused", | |
| "happy", | |
| "neutral", | |
| "focused", | |
| "relaxed", | |
| "active", | |
| ] | |
| self.rhythm_patterns = list(RhythmaModulationEngine.RHYTHM_CONFIGS.keys()) | |
| self.groq_client = None | |
| self.use_groq = use_groq and GROQ_AVAILABLE | |
| self.use_embeddings = use_embeddings | |
| self.embedding_model = None | |
| self.emotional_embeddings = {} | |
| self.rhythm_embeddings = {} | |
| self._embedding_init_attempted = False | |
| if self.use_groq: | |
| self._initialize_groq_client() | |
| def _initialize_groq_client(self): | |
| api_key = os.environ.get("GROQ_API_KEY") | |
| if not api_key: | |
| LOGGER.warning("GROQ_API_KEY not found. Groq features disabled.") | |
| self.use_groq = False | |
| return | |
| try: | |
| self.groq_client = Groq(api_key=api_key) | |
| except Exception: | |
| LOGGER.exception("Failed to initialize Groq client.") | |
| self.use_groq = False | |
| def _ensure_embeddings_loaded(self): | |
| if not self.use_embeddings or self._embedding_init_attempted: | |
| return | |
| self._embedding_init_attempted = True | |
| try: | |
| from sentence_transformers import SentenceTransformer | |
| self.embedding_model = SentenceTransformer("all-MiniLM-L6-v2") | |
| self.emotional_embeddings = { | |
| state: self.embedding_model.encode([state])[0] | |
| for state in self.emotional_states | |
| } | |
| self.rhythm_embeddings = { | |
| pattern: self.embedding_model.encode([pattern])[0] | |
| for pattern in self.rhythm_patterns | |
| } | |
| except ImportError: | |
| LOGGER.info( | |
| "SentenceTransformer not installed. Falling back to keyword matching." | |
| ) | |
| self.use_embeddings = False | |
| except Exception: | |
| LOGGER.exception("Failed to initialize SentenceTransformer embeddings.") | |
| self.use_embeddings = False | |
| self.embedding_model = None | |
| self.emotional_embeddings = {} | |
| self.rhythm_embeddings = {} | |
| def detect_emotion_with_groq(self, input_text): | |
| if not self.use_groq or not self.groq_client: | |
| return None | |
| prompt = ( | |
| "Analyze the user's feeling described below.\n" | |
| "Identify the single MOST prominent emotional state or intention from the following list:\n" | |
| f"{', '.join(self.emotional_states)}\n" | |
| "Focus on the core feeling expressed. Respond with ONLY the chosen state/intention from the list.\n" | |
| f"User's feeling: \"{input_text}\"\n" | |
| "State/Intention:" | |
| ) | |
| try: | |
| chat_completion = self.groq_client.chat.completions.create( | |
| messages=[{"role": "user", "content": prompt}], | |
| model="llama-3.3-70b-versatile", | |
| max_tokens=15, | |
| temperature=0.2, | |
| stop=["\n"], | |
| ) | |
| detected_emotion = chat_completion.choices[0].message.content.strip().lower() | |
| if detected_emotion in self.emotional_states: | |
| return detected_emotion | |
| return self.get_closest_emotional_state(detected_emotion) | |
| except Exception: | |
| LOGGER.exception("Groq emotion detection failed.") | |
| return None | |
| def get_closest_emotional_state(self, input_text): | |
| if not input_text: | |
| return "neutral" | |
| input_text_lower = input_text.lower() | |
| words = set(input_text_lower.split()) | |
| for state in self.emotional_states: | |
| if state in words or state in input_text_lower: | |
| return state | |
| if "focus" in input_text_lower or "deep work" in input_text_lower: | |
| return "focused" | |
| self._ensure_embeddings_loaded() | |
| if self.embedding_model and self.emotional_embeddings: | |
| try: | |
| input_embedding = self.embedding_model.encode([input_text])[0] | |
| return max( | |
| self.emotional_embeddings, | |
| key=lambda state: _cosine_similarity( | |
| input_embedding, self.emotional_embeddings[state] | |
| ), | |
| ) | |
| except Exception: | |
| LOGGER.exception("Semantic emotion matching failed.") | |
| return "neutral" | |
| def get_closest_rhythm_pattern(self, input_text=None, emotional_state=None): | |
| if emotional_state: | |
| mapping = { | |
| "anxious": "calm", | |
| "stressed": "relaxed", | |
| "calm": "calm", | |
| "sad": "relaxed", | |
| "angry": "active", | |
| "fearful": "calm", | |
| "confused": "focused", | |
| "happy": "active", | |
| "neutral": "calm", | |
| "focused": "focused", | |
| "relaxed": "relaxed", | |
| "active": "active", | |
| } | |
| return mapping.get(emotional_state, "calm") | |
| self._ensure_embeddings_loaded() | |
| if input_text and self.embedding_model and self.rhythm_embeddings: | |
| try: | |
| input_embedding = self.embedding_model.encode([input_text])[0] | |
| return max( | |
| self.rhythm_embeddings, | |
| key=lambda pattern: _cosine_similarity( | |
| input_embedding, self.rhythm_embeddings[pattern] | |
| ), | |
| ) | |
| except Exception: | |
| LOGGER.exception("Semantic rhythm matching failed.") | |
| return "calm" | |
| def build_session_profile(self, emotional_state, rhythm_pattern): | |
| if emotional_state in SESSION_PRESETS: | |
| preset = SESSION_PRESETS[emotional_state] | |
| else: | |
| preset = SESSION_PRESETS["neutral"] | |
| profile = preset.to_dict() | |
| profile["pattern"] = rhythm_pattern or preset.pattern | |
| return profile | |
| def apply_profile_overrides( | |
| self, | |
| profile, | |
| tone_center=None, | |
| modulation_type=None, | |
| session_pattern=None, | |
| ): | |
| shaped_profile = dict(profile) | |
| if tone_center is not None and tone_center > 0: | |
| shaped_profile["tone_center"] = tone_center | |
| if modulation_type: | |
| shaped_profile["modulation_type"] = modulation_type | |
| if session_pattern: | |
| shaped_profile["pattern"] = session_pattern | |
| return shaped_profile | |
| def transcribe_audio(self, audio_path): | |
| if not self.use_groq or not self.groq_client: | |
| return None, "Transcription disabled: Groq client not available or API key missing." | |
| if not audio_path or not os.path.exists(audio_path): | |
| return None, "Transcription failed: Audio file path is invalid or missing." | |
| try: | |
| with open(audio_path, "rb") as audio_file: | |
| response = self.groq_client.audio.transcriptions.create( | |
| file=(os.path.basename(audio_path), audio_file.read()), | |
| model="whisper-large-v3", | |
| response_format="json", | |
| ) | |
| return response.text, None | |
| except Exception as exc: | |
| LOGGER.exception("Groq transcription failed.") | |
| return None, f"Error during Groq transcription: {exc}" | |
| def analyze_input(self, input_text=None, audio_path=None): | |
| result = AnalysisResult() | |
| text_to_analyze = None | |
| try: | |
| if audio_path and self.use_groq: | |
| transcribed_text, transcription_error = self.transcribe_audio(audio_path) | |
| if transcription_error: | |
| result.error = transcription_error | |
| result.transcription = f"[Transcription Error: {transcription_error}]" | |
| elif transcribed_text: | |
| result.transcription = transcribed_text | |
| text_to_analyze = transcribed_text | |
| if not text_to_analyze and input_text: | |
| text_to_analyze = input_text | |
| if text_to_analyze: | |
| detected_emotion = None | |
| if self.use_groq: | |
| detected_emotion = self.detect_emotion_with_groq(text_to_analyze) | |
| result.emotional_state = detected_emotion or self.get_closest_emotional_state( | |
| text_to_analyze | |
| ) | |
| else: | |
| result.emotional_state = "neutral" | |
| result.rhythm_pattern = self.get_closest_rhythm_pattern( | |
| input_text=text_to_analyze, | |
| emotional_state=result.emotional_state, | |
| ) | |
| result.session_profile = self.build_session_profile( | |
| result.emotional_state, | |
| result.rhythm_pattern, | |
| ) | |
| except Exception as exc: | |
| LOGGER.exception("Unexpected error during input analysis.") | |
| result = AnalysisResult( | |
| session_profile=self.build_session_profile("neutral", "calm"), | |
| transcription=result.transcription, | |
| error=f"Unexpected error during input analysis: {exc}", | |
| ) | |
| return result.to_dict() | |