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

Update rhythma.py

Browse files
Files changed (1) hide show
  1. rhythma.py +406 -286
rhythma.py CHANGED
@@ -1,291 +1,411 @@
1
- import os
2
- import gradio as gr
3
  import numpy as np
4
- import matplotlib
5
- matplotlib.use('Agg') # Set backend BEFORE importing pyplot
6
  import matplotlib.pyplot as plt
 
 
7
  from PIL import Image
 
 
8
  import soundfile as sf
9
- import tempfile
10
- import time
11
- from rhythma import RhythmaModulationEngine, RhythmaSymphAICore # Assuming rhythma.py is in the same directory
12
-
13
- # --- Environment Variable Check ---
14
- GROQ_API_KEY = os.environ.get("GROQ_API_KEY")
15
- use_groq = bool(GROQ_API_KEY) # True only if key exists and is not empty
16
-
17
- if not use_groq:
18
- print("*"*40)
19
- print("⚠️ WARNING: GROQ_API_KEY not found or empty in environment variables.")
20
- print(" Groq LLM analysis and audio transcription features will be disabled.")
21
- print(" Falling back to local analysis methods.")
22
- print("*"*40)
23
- else:
24
- print("✅ GROQ_API_KEY found. Enabling Groq features.")
25
- # --- End Environment Variable Check ---
26
-
27
-
28
- # --- Initialize Core Components ---
29
  try:
30
- # Pass the determined use_groq flag to the core
31
- symphai_core = RhythmaSymphAICore(use_groq=use_groq)
32
- except Exception as e:
33
- print(f"❌ FATAL ERROR: Could not initialize RhythmaSymphAICore: {e}")
34
- # Handle fatal error appropriately - maybe exit or disable functionality
35
- symphai_core = None # Indicate failure
36
- # --- End Initialization ---
37
-
38
-
39
- # --- Core Functions ---
40
- def analyze_input(input_text=None, audio_input=None):
41
- """Analyze user input using the SymphAI Core."""
42
- if symphai_core is None:
43
- return {"error": "Analysis Core failed to initialize."}
44
-
45
- # Ensure audio_input is a filepath string or None
46
- audio_filepath = audio_input if isinstance(audio_input, str) else None
47
-
48
- # Pass to SymphAI Core for analysis
49
- # Add default empty string for input_text if None, as core expects string or None
50
- return symphai_core.analyze_input(input_text or "", audio_filepath)
51
-
52
-
53
- def generate_modulated_experience(analysis_result, base_freq=None, modulation_type="sine", rhythm_pattern=None, duration=5):
54
- """Generate a complete modulated experience based on analysis and parameters."""
55
- print(f"DEBUG: generate_modulated_experience received analysis: {analysis_result}")
56
- print(f"DEBUG: Overrides - Freq: {base_freq}, Mod: {modulation_type}, Rhythm: {rhythm_pattern}, Dur: {duration}")
57
-
58
- # --- Input Validation ---
59
- if not isinstance(analysis_result, dict):
60
- error_msg = "Internal Error: Analysis result is not in the expected format."
61
- print(f"❌ {error_msg}")
62
- return error_msg, None, None, None, None
63
-
64
- if "error" in analysis_result and analysis_result["error"]:
65
- error_msg = f"Analysis Error: {analysis_result['error']}"
66
- print(f"❌ {error_msg}")
67
- # Return the error message clearly for the analysis output
68
- return error_msg, None, None, None, None
69
-
70
- # Ensure required keys exist, even if defaults were used in analysis
71
- emotional_state = analysis_result.get("emotional_state", "neutral") # Default if missing
72
- rhythm_pattern_from_analysis = analysis_result.get("rhythm_pattern", "calm") # Default if missing
73
-
74
- # --- Determine Final Parameters ---
75
- # Use manual override if provided and valid, otherwise use analysis result
76
- final_rhythm_pattern = rhythm_pattern if rhythm_pattern else rhythm_pattern_from_analysis
77
- # Use manual frequency override ONLY if it's > 0
78
- final_base_freq = base_freq if base_freq and base_freq > 0 else None # Pass None to let engine use emotion/default
79
-
80
- print(f"DEBUG: Engine Params - Emotion: {emotional_state}, Freq Override: {final_base_freq}, Rhythm: {final_rhythm_pattern}, Mod: {modulation_type}")
81
-
82
- try:
83
- # --- Initialize the Rhythma Engine ---
84
- engine = RhythmaModulationEngine(
85
- base_freq=final_base_freq, # Engine handles None: uses emotional_state or default
86
- modulation_type=modulation_type,
87
- rhythm_pattern=final_rhythm_pattern,
88
- emotional_state=emotional_state if not final_base_freq else None # Pass emotion only if freq isn't overridden
89
- )
90
-
91
- # --- Generate Outputs ---
92
- timestamp = int(time.time())
93
- temp_dir = tempfile.gettempdir()
94
- # Ensure temp_dir exists (useful in some restricted environments)
95
- os.makedirs(temp_dir, exist_ok=True)
96
- audio_file = os.path.join(temp_dir, f"rhythma_{timestamp}.wav")
97
-
98
- # Generate and save audio
99
- saved_audio_path = engine.save_audio(duration, audio_file)
100
- if not saved_audio_path: # Check if saving failed
101
- raise RuntimeError("Failed to save generated audio file.")
102
-
103
- # Generate waveform visualization (Plot)
104
- fig = engine.visualize_waveform(duration)
105
-
106
- # Get simple waveform image (PIL Image)
107
- waveform_image = engine.get_waveform_image()
108
-
109
- # Get complete analysis text from the engine's perspective
110
- analysis_text = engine.get_complete_analysis()
111
-
112
- # Get symbolic interpretation
113
- symbolic = engine.get_symbolic_interpretation()
114
-
115
- print("✅ Modulation experience generated successfully.")
116
- return analysis_text, saved_audio_path, fig, waveform_image, symbolic
117
-
118
- except Exception as e:
119
- error_msg = f"Error during Rhythma generation: {e}"
120
- print(f"❌ {error_msg}")
121
- import traceback
122
- traceback.print_exc()
123
- # Return error message for analysis, and None for other outputs
124
- return error_msg, None, None, None, None
125
-
126
-
127
- def rhythma_experience(
128
- input_text, audio_input,
129
- override_freq=None,
130
- override_modulation="sine",
131
- override_rhythm=None,
132
- duration=5
133
- ):
134
- """Complete Rhythma experience pipeline: Analysis -> Generation"""
135
- print("\n--- Starting New Rhythma Experience ---")
136
- # Clean up input text
137
- input_text = input_text.strip() if input_text else ""
138
-
139
- # --- Step 1: Analyze input ---
140
- # Ensure override_freq is float or None
141
- try:
142
- freq_override_value = float(override_freq) if override_freq is not None else 0.0
143
- except (ValueError, TypeError):
144
- freq_override_value = 0.0 # Default to 0 if invalid input
145
-
146
- analysis = analyze_input(input_text, audio_input)
147
-
148
- # --- Step 2: Generate modulated experience ---
149
- # Pass analysis results and overrides to the generation function
150
- analysis_text, audio_file, fig, waveform_image, symbolic = generate_modulated_experience(
151
- analysis,
152
- base_freq=freq_override_value, # Pass the validated float/int
153
- modulation_type=override_modulation,
154
- rhythm_pattern=override_rhythm if override_rhythm else None, # Pass None if dropdown default is selected
155
- duration=duration
156
- )
157
-
158
- # --- Step 3: Prepare Outputs ---
159
- # Get transcription from analysis result (will be empty string if no audio/transcription)
160
- transcription = analysis.get("transcription", "") if isinstance(analysis, dict) else ""
161
- # If analysis itself failed, analysis_text will contain the error message from generate_modulated_experience
162
- # If only transcription failed, it might be in the transcription field
163
-
164
- # Handle potential None figure if generation failed
165
- plot_output = fig if fig else None # Gradio handles None for Plot output
166
-
167
- print("--- Rhythma Experience Complete ---")
168
- # Return all outputs for Gradio interface
169
- return analysis_text, audio_file, plot_output, waveform_image, symbolic, transcription
170
-
171
- # --- Create the Gradio Interface ---
172
- def create_interface():
173
- with gr.Blocks(theme=gr.themes.Soft(), title="Rhythma: The Living Modulation Engine") as demo:
174
- gr.Markdown("# Rhythma: The Living Modulation Engine")
175
- gr.Markdown("### Dynamic rhythm-based sound modulation for wellbeing")
176
-
177
- if not use_groq:
178
- gr.Warning("Running with limited functionality: GROQ_API_KEY not found. "
179
- "Advanced AI analysis and audio transcription are disabled.")
180
-
181
- with gr.Row():
182
- with gr.Column(scale=1):
183
- gr.Markdown("**1. Describe Your State or Intention**")
184
- input_text = gr.Textbox(
185
- label="How are you feeling, or what is your intention?",
186
- placeholder="e.g., 'feeling stressed about work', 'want to relax', 'need focus'...",
187
- lines=3
188
- )
189
-
190
- gr.Markdown("**Optional: Use Your Voice (Requires Groq API Key)**")
191
- audio_input = gr.Audio(
192
- sources=["microphone"], # Prioritize microphone
193
- type="filepath", # RhythmaSymphAICore expects a filepath
194
- label="Record or Upload Audio" if use_groq else "Audio Input (Disabled)",
195
- interactive=use_groq # Disable if Groq not available
196
- )
197
-
198
- with gr.Accordion("Advanced Settings (Optional Overrides)", open=False):
199
- override_freq = gr.Slider(
200
- minimum=0, maximum=1000, value=0, step=1,
201
- label="Override Frequency (Hz)",
202
- info="Leave at 0 to use automatic frequency based on analysis."
203
- )
204
- override_modulation = gr.Dropdown(
205
- choices=["sine", "pulse", "chirp"],
206
- value="sine",
207
- label="Override Modulation Type"
208
- )
209
- # Get available patterns from the engine instance
210
- available_patterns = list(RhythmaModulationEngine().rhythm_configs.keys())
211
- override_rhythm = gr.Dropdown(
212
- choices=[None] + available_patterns, # Add None option for automatic
213
- value=None, # Default to automatic
214
- label="Override Rhythm Pattern",
215
- info="Leave blank to use automatic pattern based on analysis."
216
- )
217
- duration = gr.Slider(
218
- minimum=3, maximum=60, value=10, step=1,
219
- label="Duration (seconds)"
220
- )
221
-
222
- generate_button = gr.Button("Generate Rhythma Experience", variant="primary", scale=2)
223
-
224
- with gr.Column(scale=2):
225
- gr.Markdown("**2. Experience Your Rhythma Soundscape**")
226
- analysis_output = gr.Textbox(label="Rhythma Analysis & Guidance", lines=8, interactive=False)
227
- with gr.Row():
228
- audio_output = gr.Audio(label="Modulated Audio", type="filepath", interactive=False)
229
- waveform_simple = gr.Image(label="Base Waveform", interactive=False, height=100, width=200)
230
- waveform_plot = gr.Plot(label="Detailed Waveform & Spectrogram", interactive=False)
231
- symbolic_output = gr.Textbox(label="Symbolic Interpretation", interactive=False)
232
- # Conditionally visible transcription output
233
- transcription_output = gr.Textbox(
234
- label="Transcribed Audio (If Provided)",
235
- interactive=False,
236
- visible=use_groq # Only show if Groq is potentially usable
237
- )
238
-
239
- # Define button action
240
- generate_button.click(
241
- fn=rhythma_experience,
242
- inputs=[
243
- input_text, audio_input,
244
- override_freq, override_modulation, override_rhythm,
245
- duration
246
- ],
247
- outputs=[
248
- analysis_output, audio_output,
249
- waveform_plot, waveform_simple, symbolic_output,
250
- transcription_output
251
- ]
252
- )
253
-
254
- # Add Examples
255
- gr.Examples(
256
- examples=[
257
- ["I'm feeling anxious about my upcoming presentation.", None, 0, "sine", None, 10],
258
- ["I feel at peace and grounded today.", None, 0, "sine", None, 15],
259
- ["I need to focus on my work but keep getting distracted.", None, 0, "sine", None, 20],
260
- ["Feeling overwhelmed with responsibilities.", None, 0, "sine", None, 10],
261
- ["Excited about my vacation next week!", None, 0, "sine", None, 10],
262
- ["Just want to relax after a long day.", None, 0, "sine", "relaxed", 30], # Example with override
263
- ["Feeling sad and low energy.", None, 0, "sine", None, 15],
264
- ],
265
- inputs=[input_text, audio_input, override_freq, override_modulation, override_rhythm, duration],
266
- outputs=[analysis_output, audio_output, waveform_plot, waveform_simple, symbolic_output, transcription_output],
267
- fn=rhythma_experience, # Ensure examples also run the main function
268
- cache_examples=False # Maybe disable caching during development
269
- )
270
-
271
- gr.Markdown("---")
272
- gr.Markdown("""
273
- ## About Rhythma
274
- Rhythma creates personalized soundscapes using frequency modulation based on your described emotional state or intention.
275
- It leverages AI analysis (enhanced with Groq if available) and principles of rhythmic sound design.
276
- **Note:** This is an experimental tool. The frequencies and interpretations are based on various theories and are not medical advice.
277
- © 2024 Your Rhythma Project
278
- """)
279
-
280
- return demo
281
-
282
- # --- Run the Gradio App ---
283
- if __name__ == "__main__":
284
- if symphai_core is None:
285
- print("\n❌ Cannot launch Gradio app because RhythmaSymphAICore failed to initialize.\n")
286
- else:
287
- print("\n🚀 Launching Rhythma Gradio Interface...")
288
- app_demo = create_interface()
289
- # Set share=True if you need a public link (useful for testing deployment)
290
- # Set debug=True for more verbose logs during development
291
- app_demo.launch()#debug=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import numpy as np
 
 
2
  import matplotlib.pyplot as plt
3
+ from scipy import signal
4
+ # import pandas as pd # pandas wasn't used, commented out
5
  from PIL import Image
6
+ import io
7
+ from sklearn.metrics.pairwise import cosine_similarity
8
  import soundfile as sf
9
+ import os
10
+ import traceback # For better error logging
11
+
12
+ # --- Optional Dependency Handling ---
13
+ try:
14
+ from groq import Groq
15
+ GROQ_AVAILABLE = True
16
+ except ImportError:
17
+ GROQ_AVAILABLE = False
18
+ print("⚠️ Groq package not installed. LLM analysis and transcription disabled.")
19
+
 
 
 
 
 
 
 
 
 
20
  try:
21
+ from sentence_transformers import SentenceTransformer
22
+ SENTENCE_TRANSFORMER_AVAILABLE = True
23
+ except ImportError:
24
+ SENTENCE_TRANSFORMER_AVAILABLE = False
25
+ print("⚠️ SentenceTransformer not installed. Falling back to simple text matching for analysis.")
26
+ # --- End Optional Dependency Handling ---
27
+
28
+ class RhythmaModulationEngine:
29
+ """
30
+ Rhythma: The Living Modulation Engine
31
+ A dynamic rhythm-based audio modulation system that creates responsive
32
+ sound experiences based on rhythm patterns and emotional states.
33
+ """
34
+
35
+ def __init__(self, base_freq=None, modulation_type="sine", rhythm_pattern=None, emotional_state=None):
36
+ """
37
+ Initialize the RhythmaModulationEngine.
38
+
39
+ Args:
40
+ base_freq (float, optional): The base frequency in Hz. Overridden by emotional_state if provided.
41
+ modulation_type (str): Type of modulation (sine, pulse, chirp). Defaults to "sine".
42
+ rhythm_pattern (str, optional): Pattern type (calm, active, focused, relaxed). Defaults to "calm".
43
+ emotional_state (str, optional): Emotional state (anxious, stressed, calm, etc.). Maps to specific frequencies.
44
+ """
45
+ self.modulation_type = modulation_type
46
+ self.sample_rate = 44100 # Standard audio sample rate
47
+
48
+ # Define frequency mappings for emotional states (Example frequencies)
49
+ self.emotional_frequencies = {
50
+ "anxious": 396,
51
+ "stressed": 528,
52
+ "calm": 741,
53
+ "sad": 417,
54
+ "angry": 852,
55
+ "fearful": 639,
56
+ "confused": 285,
57
+ "happy": 432,
58
+ "neutral": 440, # Added neutral state
59
+ "focused": 639, # Example mapping for focus intention
60
+ "relaxed": 741, # Example mapping for relax intention
61
+ "active": 528, # Example mapping for active intention
62
+ }
63
+
64
+ # Detailed information about emotional states/frequencies
65
+ self.emotional_info = {
66
+ "anxious": {"name": "Liberating Guilt and Fear", "advice": "The 396 Hz frequency may help release fear and guilt."},
67
+ "stressed": {"name": "Transformation and Miracles", "advice": "The 528 Hz frequency is associated with transformation."},
68
+ "calm": {"name": "Awakening Intuition", "advice": "The 741 Hz frequency is associated with awakening intuition."},
69
+ "sad": {"name": "Facilitating Change", "advice": "The 417 Hz frequency is linked to facilitating change."},
70
+ "angry": {"name": "Returning to Spiritual Order", "advice": "The 852 Hz frequency may aid in returning to inner strength."},
71
+ "fearful": {"name": "Connecting Relationships", "advice": "The 639 Hz frequency is associated with connecting relationships."},
72
+ "confused": {"name": "Quantum Cognition", "advice": "The 285 Hz frequency is believed to influence energy fields."},
73
+ "happy": {"name": "Harmonizing Vibrations", "advice": "The 432 Hz frequency is associated with natural harmony."},
74
+ "neutral": {"name": "Grounded Presence", "advice": "The 440 Hz frequency provides a stable reference point."},
75
+ "focused": {"name": "Clarity and Connection", "advice": "The 639 Hz frequency may support focus and understanding."},
76
+ "relaxed": {"name": "Intuitive Calm", "advice": "The 741 Hz frequency is linked to intuitive states and problem-solving."},
77
+ "active": {"name": "Dynamic Energy", "advice": "The 528 Hz frequency is associated with positive transformation."},
78
+ }
79
+
80
+ # Configure rhythm patterns
81
+ self.rhythm_configs = {
82
+ "calm": {"mod_depth": 0.15, "mod_freq": 0.5, "pulse_width": 0.7, "phase_shift": 0.1, "harmonics": [1.0, 0.5, 0.25, 0.125]},
83
+ "active": {"mod_depth": 0.4, "mod_freq": 2.5, "pulse_width": 0.3, "phase_shift": 0.3, "harmonics": [1.0, 0.7, 0.5, 0.3]},
84
+ "focused": {"mod_depth": 0.25, "mod_freq": 1.5, "pulse_width": 0.5, "phase_shift": 0.2, "harmonics": [1.0, 0.6, 0.3, 0.15]},
85
+ "relaxed": {"mod_depth": 0.2, "mod_freq": 0.3, "pulse_width": 0.8, "phase_shift": 0.05, "harmonics": [1.0, 0.4, 0.2, 0.1]}
86
+ }
87
+
88
+ # Symbolic mapping for rhythm patterns
89
+ self.symbolic_mapping = {
90
+ "calm": "Resonating in the Circle Archetype: completion, wholeness, presence",
91
+ "active": "Resonating in the Spiral Archetype: flow, transition, emergence",
92
+ "focused": "Resonating in the Triangle Archetype: clarity, direction, purpose",
93
+ "relaxed": "Resonating in the Wave Archetype: fluidity, acceptance, surrender"
94
+ }
95
+
96
+ # Determine emotional state and base frequency
97
+ valid_emotional_state = emotional_state if emotional_state and emotional_state in self.emotional_frequencies else None
98
+ self.emotional_state = valid_emotional_state
99
+
100
+ if self.emotional_state:
101
+ self.base_freq = self.emotional_frequencies[self.emotional_state]
102
+ elif base_freq and base_freq > 0: # Check if base_freq override is valid
103
+ self.base_freq = base_freq
104
+ # Try to find a state close to the frequency for info purposes
105
+ min_diff = float('inf')
106
+ closest_state = None
107
+ for state, freq in self.emotional_frequencies.items():
108
+ diff = abs(freq - base_freq)
109
+ if diff < min_diff:
110
+ min_diff = diff
111
+ closest_state = state
112
+ # Only assign if reasonably close (e.g., within 10 Hz)
113
+ if min_diff <= 10:
114
+ self.emotional_state = closest_state # Use for info display only
115
+ else:
116
+ self.emotional_state = None # No specific emotional state tied
117
+ else:
118
+ self.emotional_state = "neutral" # Default state if no emotion/freq provided
119
+ self.base_freq = self.emotional_frequencies[self.emotional_state]
120
+
121
+
122
+ # Set rhythm pattern
123
+ valid_rhythm_pattern = rhythm_pattern if rhythm_pattern and rhythm_pattern in self.rhythm_configs else None
124
+ self.rhythm_pattern = valid_rhythm_pattern or "calm" # Default to calm if not provided or invalid
125
+
126
+ # Get current rhythm config
127
+ self.config = self.rhythm_configs.get(self.rhythm_pattern, self.rhythm_configs["calm"])
128
+
129
+ def _generate_base_wave(self, duration):
130
+ """Generate the base carrier wave"""
131
+ t = np.linspace(0, duration, int(self.sample_rate * duration), endpoint=False)
132
+ # Initial simple sine wave
133
+ base_wave = np.sin(2 * np.pi * self.base_freq * t)
134
+
135
+ # Apply harmonics for richer base sound *before* modulation
136
+ harmonics = self.config["harmonics"]
137
+ rich_wave = np.zeros_like(base_wave)
138
+ for i, harmonic_amp in enumerate(harmonics):
139
+ harmonic_freq = self.base_freq * (i + 1)
140
+ # Ensure harmonic frequency does not exceed Nyquist limit
141
+ if harmonic_freq < self.sample_rate / 2:
142
+ rich_wave += harmonic_amp * np.sin(2 * np.pi * harmonic_freq * t)
143
+
144
+ # Normalize the rich base wave before modulation
145
+ if np.max(np.abs(rich_wave)) > 0:
146
+ rich_wave = rich_wave / np.max(np.abs(rich_wave))
147
+ else:
148
+ rich_wave = base_wave # Fallback if harmonics resulted in zero
149
+
150
+ return t, rich_wave
151
+
152
+
153
+ def _apply_sine_modulation(self, t, carrier):
154
+ """Apply sine wave amplitude modulation"""
155
+ mod_freq = self.config["mod_freq"]
156
+ mod_depth = self.config["mod_depth"]
157
+ mod_env = 1.0 + mod_depth * np.sin(2 * np.pi * mod_freq * t + self.config["phase_shift"])
158
+ return carrier * mod_env
159
+
160
+ def _apply_pulse_modulation(self, t, carrier):
161
+ """Apply pulse wave amplitude modulation"""
162
+ mod_freq = self.config["mod_freq"]
163
+ mod_depth = self.config["mod_depth"]
164
+ pulse_width = self.config["pulse_width"]
165
+ pulse = 0.5 * (signal.square(2 * np.pi * mod_freq * t, duty=pulse_width) + 1) # 0 to 1
166
+ mod_env = 1.0 - mod_depth + mod_depth * pulse # Modulates between (1-depth) and 1
167
+ return carrier * mod_env
168
+
169
+ def _apply_chirp_modulation(self, t, carrier):
170
+ """Apply frequency chirp modulation (applied differently)"""
171
+ # Chirp modulation modifies frequency directly, not amplitude envelope
172
+ # This implementation is more complex and might replace the base wave generation
173
+ # For simplicity, let's keep amplitude modulation for 'chirp' but with a varying mod freq
174
+
175
+ # Simple approach: vary the *modulation frequency* over time (like a siren)
176
+ start_mod_freq = max(0.1, self.config["mod_freq"] / 2) # Avoid 0 Hz
177
+ end_mod_freq = self.config["mod_freq"] * 2
178
+ instantaneous_mod_freq = np.linspace(start_mod_freq, end_mod_freq, len(t))
179
+
180
+ mod_depth = self.config["mod_depth"]
181
+ # Integrate frequency to get phase: 2 * pi * integral(f(t) dt)
182
+ phase = 2 * np.pi * np.cumsum(instantaneous_mod_freq) / self.sample_rate
183
+ mod_env = 1.0 + mod_depth * np.sin(phase + self.config["phase_shift"])
184
+ return carrier * mod_env
185
+
186
+ def generate_modulated_wave(self, duration):
187
+ """
188
+ Generate modulated audio wave based on current settings.
189
+ Applies harmonics to the base wave first, then applies modulation.
190
+ """
191
+ t, base_carrier = self._generate_base_wave(duration) # Base carrier now includes harmonics
192
+
193
+ # Apply the selected amplitude modulation type
194
+ if self.modulation_type == "sine":
195
+ modulated = self._apply_sine_modulation(t, base_carrier)
196
+ elif self.modulation_type == "pulse":
197
+ modulated = self._apply_pulse_modulation(t, base_carrier)
198
+ elif self.modulation_type == "chirp":
199
+ # Using the amplitude modulation with varying frequency approach
200
+ modulated = self._apply_chirp_modulation(t, base_carrier)
201
+ else:
202
+ modulated = base_carrier # Default to unmodulated rich carrier
203
+
204
+ # Final normalization to prevent clipping
205
+ max_amp = np.max(np.abs(modulated))
206
+ if max_amp > 0:
207
+ normalized = 0.9 * modulated / max_amp # Use 0.9 to leave headroom
208
+ else:
209
+ normalized = modulated # Avoid division by zero if signal is silent
210
+
211
+ return normalized
212
+
213
+ def save_audio(self, duration, file_path=None):
214
+ """Generate and save audio to a file"""
215
+ audio = self.generate_modulated_wave(duration)
216
+ file_path = file_path or f"rhythma_{self.base_freq}Hz_{self.rhythm_pattern}.wav"
217
+ try:
218
+ sf.write(file_path, audio, self.sample_rate)
219
+ print(f"Audio saved to: {file_path}")
220
+ return file_path
221
+ except Exception as e:
222
+ print(f"Error saving audio file: {e}")
223
+ traceback.print_exc()
224
+ return None # Return None if saving fails
225
+
226
+
227
+ def visualize_waveform(self, duration):
228
+ """Generate visualization of the modulated waveform"""
229
+ # Generate a shorter segment for visualization consistency
230
+ vis_duration = min(duration, 0.5) # Shorter duration for clearer plot
231
+ plot_samples = int(self.sample_rate * vis_duration)
232
+
233
+ t = np.linspace(0, vis_duration, plot_samples, endpoint=False)
234
+ modulated = self.generate_modulated_wave(vis_duration) # Generate specific duration
235
+
236
+ fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(10, 6), gridspec_kw={'height_ratios': [1, 1]})
237
+
238
+ # Plot time domain (zoom in on a small section for detail)
239
+ zoom_samples = min(plot_samples, 2000) # Show max ~45ms
240
+ ax1.plot(t[:zoom_samples], modulated[:zoom_samples])
241
+ title = f'Rhythma Waveform: {self.rhythm_pattern.capitalize()} ({self.modulation_type.capitalize()})'
242
+ if self.emotional_state:
243
+ title += f' - {self.emotional_state.capitalize()} ({self.base_freq} Hz)'
244
+ else:
245
+ title += f' - {self.base_freq} Hz'
246
+ ax1.set_title(title)
247
+ ax1.set_xlabel('Time (s)')
248
+ ax1.set_ylabel('Amplitude')
249
+ ax1.grid(True)
250
+
251
+ # Plot frequency domain (spectrogram)
252
+ try:
253
+ # Use the full generated segment for spectrogram if possible
254
+ full_wave = self.generate_modulated_wave(duration)
255
+ f, t_spec, Sxx = signal.spectrogram(full_wave, self.sample_rate, nperseg=1024)
256
+ # Limit frequency display range for clarity (e.g., up to 2kHz)
257
+ freq_limit_idx = np.where(f >= 2000)[0]
258
+ if len(freq_limit_idx) > 0:
259
+ f = f[:freq_limit_idx[0]]
260
+ Sxx = Sxx[:freq_limit_idx[0], :]
261
+ else: # Handle cases where max freq is below limit
262
+ pass
263
+ # Use logarithmic scale for power if needed
264
+ pcm = ax2.pcolormesh(t_spec, f, 10 * np.log10(Sxx + 1e-9), shading='gouraud', cmap='viridis') # Log scale power
265
+ fig.colorbar(pcm, ax=ax2, label='Power (dB)') # Add colorbar
266
+ ax2.set_ylabel('Frequency (Hz)')
267
+ ax2.set_xlabel('Time (s)')
268
+ ax2.set_title('Spectrogram')
269
+
270
+ except Exception as e:
271
+ print(f"Error generating spectrogram: {e}")
272
+ ax2.set_title('Spectrogram (Error)')
273
+ ax2.text(0.5, 0.5, 'Could not generate spectrogram', horizontalalignment='center', verticalalignment='center', transform=ax2.transAxes)
274
+
275
+
276
+ plt.tight_layout(rect=[0, 0.05, 1, 1]) # Adjust layout to prevent overlap, leave space at bottom
277
+
278
+ # Add symbolic interpretation below plots
279
+ fig_text = self.get_symbolic_interpretation()
280
+ emotion_info = self.emotional_info.get(self.emotional_state, {})
281
+ if emotion_info:
282
+ fig_text += f"\n{self.base_freq} Hz - {emotion_info.get('name', '')}: {emotion_info.get('advice', '')}"
283
+ elif not self.emotional_state: # Case where only base_freq was set
284
+ fig_text += f"\nBase Frequency: {self.base_freq} Hz"
285
+
286
+
287
+ fig.text(0.5, 0.01, fig_text, ha='center', va='bottom', fontsize=9, style='italic', wrap=True)
288
+
289
+ return fig
290
+
291
+
292
+ def get_waveform_image(self):
293
+ """Generate a simple waveform image as a PIL Image"""
294
+ # Generate a short, clear representation of the *base* frequency wave
295
+ duration = 0.05 # Very short duration for visualization
296
+ t = np.linspace(0, duration, int(self.sample_rate * duration), False)
297
+ # Use the base frequency determined in __init__
298
+ tone = np.sin(2 * np.pi * self.base_freq * t)
299
+
300
+ plt.figure(figsize=(6, 2)) # Smaller figure for simple image
301
+ plt.plot(t, tone)
302
+ # plt.title(f"Base Tone: {self.base_freq} Hz") # Title might clutter small image
303
+ plt.xlabel("Time (s)")
304
+ plt.ylabel("Amplitude")
305
+ plt.ylim(-1.1, 1.1)
306
+ plt.grid(True)
307
+ plt.tight_layout()
308
+
309
+ buf = io.BytesIO()
310
+ plt.savefig(buf, format='png', bbox_inches='tight')
311
+ buf.seek(0)
312
+ plt.close() # Close the plot to free memory
313
+
314
+ return Image.open(buf)
315
+
316
+
317
+ def get_symbolic_interpretation(self):
318
+ """Return the symbolic interpretation of the current rhythm pattern"""
319
+ return self.symbolic_mapping.get(self.rhythm_pattern, "Pattern Interpretation: Default")
320
+
321
+ def get_emotional_advice(self):
322
+ """Get advice based on emotional state if available"""
323
+ if not self.emotional_state:
324
+ return "No specific emotional state identified."
325
+
326
+ emotion_info = self.emotional_info.get(self.emotional_state, {})
327
+ return emotion_info.get('advice', "General well-being advice applies.")
328
+
329
+
330
+ def get_complete_analysis(self):
331
+ """Get a complete analysis including emotional and rhythmic information"""
332
+ analysis = []
333
+
334
+ if self.emotional_state:
335
+ emotion_info = self.emotional_info.get(self.emotional_state, {})
336
+ analysis.append(f"Detected State/Intention: {self.emotional_state.capitalize()}")
337
+ analysis.append(f"Resonant Frequency: {self.base_freq} Hz - {emotion_info.get('name', 'Frequency Information')}")
338
+ analysis.append(f"Guidance: {emotion_info.get('advice', 'Focus on the sound.')}")
339
+ else:
340
+ # This case happens if only override_freq was used and it didn't map closely to a state
341
+ analysis.append(f"Using Manual Frequency: {self.base_freq} Hz")
342
+ analysis.append("Guidance: Tune into the custom frequency.")
343
+
344
+ analysis.append(f"Rhythm Pattern: {self.rhythm_pattern.capitalize()}")
345
+ analysis.append(f"Symbolic Interpretation: {self.get_symbolic_interpretation()}")
346
+ analysis.append(f"Modulation Type: {self.modulation_type.capitalize()}")
347
+
348
+ return "\n\n".join(analysis)
349
+
350
+
351
+ class RhythmaSymphAICore:
352
+ """
353
+ SymphAI Core - Interprets input to determine emotional state and rhythm pattern.
354
+ Handles text and audio input, utilizing Groq LLM and Sentence Transformers if available.
355
+ """
356
+
357
+ def __init__(self, use_groq=True):
358
+ """Initialize the SymphAI Core"""
359
+ # Expanded emotional states / intentions
360
+ self.emotional_states = [
361
+ "anxious", "stressed", "calm", "sad",
362
+ "angry", "fearful", "confused", "happy",
363
+ "neutral", "focused", "relaxed", "active", # Added intentions
364
+ ]
365
+
366
+ # Default rhythm patterns
367
+ self.rhythm_patterns = list(RhythmaModulationEngine().rhythm_configs.keys()) # Get from engine
368
+
369
+ # Initialize Groq client if available and requested
370
+ self.groq_client = None
371
+ self.use_groq = use_groq and GROQ_AVAILABLE
372
+ if self.use_groq:
373
+ api_key = os.environ.get("GROQ_API_KEY")
374
+ if api_key:
375
+ try:
376
+ self.groq_client = Groq(api_key=api_key)
377
+ print("✅ Groq client initialized successfully.")
378
+ except Exception as e:
379
+ print(f"⚠️ Failed to initialize Groq client: {str(e)}")
380
+ self.use_groq = False # Disable Groq if init fails
381
+ else:
382
+ print("⚠️ GROQ_API_KEY environment variable not found. Groq features disabled.")
383
+ self.use_groq = False
384
+
385
+ # Initialize sentence transformer for semantic matching if available
386
+ self.embedding_model = None
387
+ self.emotional_embeddings = {}
388
+ self.rhythm_embeddings = {}
389
+ if SENTENCE_TRANSFORMER_AVAILABLE:
390
+ try:
391
+ # Using a common, effective model
392
+ self.embedding_model = SentenceTransformer('all-MiniLM-L6-v2')
393
+ # Pre-compute embeddings for faster lookup
394
+ self.emotional_embeddings = {
395
+ state: self.embedding_model.encode([state])[0] # Get the 1D array
396
+ for state in self.emotional_states
397
+ }
398
+ self.rhythm_embeddings = {
399
+ pattern: self.embedding_model.encode([pattern])[0] # Get the 1D array
400
+ for pattern in self.rhythm_patterns
401
+ }
402
+ print("✅ SentenceTransformer initialized successfully.")
403
+ except Exception as e:
404
+ print(f"⚠️ Failed to initialize SentenceTransformer: {str(e)}. Using basic text matching.")
405
+ self.embedding_model = None # Ensure it's None if init fails
406
+ else:
407
+ print("ℹ️ SentenceTransformer not installed. Using basic text matching.")
408
+
409
+
410
+ def detect_emotion_with_groq(self, input_text):
411
+