Temporal_Exploration / rhythma_analysis.py
ciaochris's picture
Introduce session profiles for Rhythma analysis
b50aae8
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__)
@dataclass
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)
@dataclass(frozen=True)
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()