Nikita Makarov commited on
Commit
4cdaf71
·
1 Parent(s): a369c53
INSTALL_FFMPEG.md ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 🎵 Installing FFmpeg for Music Playback
2
+
3
+ FFmpeg is required to download and convert YouTube audio for playback in the app.
4
+
5
+ ## macOS Installation
6
+
7
+ ```bash
8
+ brew install ffmpeg
9
+ ```
10
+
11
+ ## Linux Installation
12
+
13
+ ```bash
14
+ # Ubuntu/Debian
15
+ sudo apt-get update
16
+ sudo apt-get install ffmpeg
17
+
18
+ # Fedora/CentOS
19
+ sudo dnf install ffmpeg
20
+
21
+ # Arch Linux
22
+ sudo pacman -S ffmpeg
23
+ ```
24
+
25
+ ## Windows Installation
26
+
27
+ 1. Download from: https://ffmpeg.org/download.html
28
+ 2. Extract to a folder (e.g., `C:\ffmpeg`)
29
+ 3. Add to PATH:
30
+ - System Properties → Environment Variables
31
+ - Add `C:\ffmpeg\bin` to Path
32
+
33
+ ## Verify Installation
34
+
35
+ ```bash
36
+ ffmpeg -version
37
+ ```
38
+
39
+ You should see version information if installed correctly.
40
+
41
+ ## Alternative: Use YouTube Links
42
+
43
+ If you can't install ffmpeg, the app will still work! It will:
44
+ - Show track information
45
+ - Provide YouTube links to listen
46
+ - All other features work normally
47
+ - Music just won't play directly in the app
48
+
49
+ ## Note
50
+
51
+ FFmpeg is only needed for downloading YouTube audio. All other features (host speech, news, podcasts, stories) work without it!
52
+
INSTALL_VOICE_INPUT.md ADDED
@@ -0,0 +1,82 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 🎤 Voice Input Installation Guide
2
+
3
+ The voice input feature requires `pyaudio`, which needs the PortAudio library to be installed first.
4
+
5
+ ## macOS Installation
6
+
7
+ 1. **Install PortAudio using Homebrew:**
8
+ ```bash
9
+ brew install portaudio
10
+ ```
11
+
12
+ 2. **Install pyaudio:**
13
+ ```bash
14
+ pip install pyaudio
15
+ ```
16
+
17
+ ## Linux Installation
18
+
19
+ 1. **Install PortAudio development libraries:**
20
+ ```bash
21
+ sudo apt-get update
22
+ sudo apt-get install portaudio19-dev python3-pyaudio
23
+ ```
24
+
25
+ Or for other distributions:
26
+ ```bash
27
+ # Fedora/CentOS
28
+ sudo dnf install portaudio-devel
29
+
30
+ # Arch Linux
31
+ sudo pacman -S portaudio
32
+ ```
33
+
34
+ 2. **Install pyaudio:**
35
+ ```bash
36
+ pip install pyaudio
37
+ ```
38
+
39
+ ## Windows Installation
40
+
41
+ 1. **Download and install PortAudio:**
42
+ - Download from: http://files.portaudio.com/download.html
43
+ - Or use pre-built wheels:
44
+ ```bash
45
+ pip install pipwin
46
+ pipwin install pyaudio
47
+ ```
48
+
49
+ ## Alternative: Use Text Input
50
+
51
+ If you can't install pyaudio, you can still use the app! The voice input feature will be disabled, but you can:
52
+ - Type your song requests in the preferences
53
+ - Use the regular radio controls
54
+ - All other features work without voice input
55
+
56
+ ## Verify Installation
57
+
58
+ After installing, test with:
59
+ ```bash
60
+ python -c "import speech_recognition as sr; print('✅ Speech recognition ready!')"
61
+ python -c "import pyaudio; print('✅ PyAudio ready!')"
62
+ ```
63
+
64
+ ## Troubleshooting
65
+
66
+ **Error: "portaudio.h not found"**
67
+ - Make sure PortAudio is installed before pyaudio
68
+ - On macOS, use Homebrew: `brew install portaudio`
69
+
70
+ **Error: "No module named 'pyaudio'"**
71
+ - Install pyaudio: `pip install pyaudio`
72
+ - Make sure PortAudio is installed first
73
+
74
+ **Microphone not detected**
75
+ - Check microphone permissions in system settings
76
+ - On macOS: System Settings → Privacy & Security → Microphone
77
+ - Make sure your app has microphone access
78
+
79
+ ## Note
80
+
81
+ Voice input is **optional**. The app works perfectly without it - you just won't be able to request songs by voice. All other features (radio, music, news, etc.) work normally.
82
+
app.py CHANGED
@@ -10,6 +10,7 @@ from config import get_config
10
  from radio_agent import RadioAgent
11
  from tts_service import TTSService
12
  from rag_system import RadioRAGSystem
 
13
 
14
  # Global state
15
  radio_state = {
@@ -17,16 +18,24 @@ radio_state = {
17
  "current_segment_index": 0,
18
  "planned_show": [],
19
  "user_preferences": {},
20
- "stop_flag": False
 
 
 
 
 
 
21
  }
22
 
23
  # Initialize services
24
  config = get_config()
25
  agent = RadioAgent(config)
26
  tts_service = TTSService(api_key=config.elevenlabs_api_key, voice_id=config.elevenlabs_voice_id)
 
27
 
28
  def save_preferences(name: str, favorite_genres: List[str], interests: List[str],
29
- podcast_interests: List[str], mood: str) -> str:
 
30
  """Save user preferences to RAG system"""
31
  preferences = {
32
  "name": name or "Friend",
@@ -37,6 +46,14 @@ def save_preferences(name: str, favorite_genres: List[str], interests: List[str]
37
  "timestamp": time.time()
38
  }
39
 
 
 
 
 
 
 
 
 
40
  radio_state["user_preferences"] = preferences
41
  agent.rag_system.store_user_preferences(preferences)
42
 
@@ -45,15 +62,16 @@ def save_preferences(name: str, favorite_genres: List[str], interests: List[str]
45
  def start_radio_stream():
46
  """Start the radio stream"""
47
  if not radio_state["user_preferences"]:
48
- return "⚠️ Please set your preferences first!", None, None, ""
49
 
50
  if radio_state["is_playing"]:
51
- return "📻 Radio is already playing!", None, None, ""
52
 
53
- # Plan the show
54
  show_plan = agent.plan_radio_show(
55
  user_preferences=radio_state["user_preferences"],
56
- duration_minutes=30
 
57
  )
58
 
59
  radio_state["planned_show"] = show_plan
@@ -61,20 +79,20 @@ def start_radio_stream():
61
  radio_state["is_playing"] = True
62
  radio_state["stop_flag"] = False
63
 
64
- return "🎵 Starting your personalized radio show...", None, None, ""
65
 
66
  def play_next_segment():
67
- """Play the next segment in the show"""
68
  if not radio_state["is_playing"]:
69
- return "⏸️ Radio stopped", None, None, ""
70
 
71
  if radio_state["stop_flag"]:
72
  radio_state["is_playing"] = False
73
- return "⏸️ Radio paused", None, None, ""
74
 
75
  if radio_state["current_segment_index"] >= len(radio_state["planned_show"]):
76
  radio_state["is_playing"] = False
77
- return "🎊 Show completed! Hope you enjoyed it.", None, None, ""
78
 
79
  # Get current segment
80
  segment = radio_state["planned_show"][radio_state["current_segment_index"]]
@@ -87,36 +105,152 @@ def play_next_segment():
87
  segment_info = format_segment_info(segment)
88
 
89
  # Generate TTS for host commentary
90
- audio_file = None
91
- if segment["type"] in ["intro", "outro", "news", "story"]:
 
 
92
  text = get_segment_text(segment)
93
  if text and tts_service.client:
94
  audio_bytes = tts_service.text_to_speech(text)
95
  if audio_bytes:
96
- audio_file = f"segment_{radio_state['current_segment_index']}.mp3"
97
- tts_service.save_audio(audio_bytes, audio_file)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
98
 
99
  elif segment["type"] == "music":
100
- # For music, generate commentary
101
  commentary = segment.get("commentary", "")
 
 
102
  if commentary and tts_service.client:
103
  audio_bytes = tts_service.text_to_speech(commentary)
104
  if audio_bytes:
105
- audio_file = f"segment_{radio_state['current_segment_index']}.mp3"
106
- tts_service.save_audio(audio_bytes, audio_file)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
107
 
108
  elif segment["type"] == "podcast":
 
109
  intro = segment.get("intro", "")
 
110
  if intro and tts_service.client:
111
  audio_bytes = tts_service.text_to_speech(intro)
112
  if audio_bytes:
113
- audio_file = f"segment_{radio_state['current_segment_index']}.mp3"
114
- tts_service.save_audio(audio_bytes, audio_file)
115
 
116
  # Progress info
117
  progress = f"Segment {radio_state['current_segment_index']}/{len(radio_state['planned_show'])}"
118
 
119
- return segment_info, audio_file, progress, get_now_playing(segment)
 
120
 
121
  def stop_radio():
122
  """Stop the radio stream"""
@@ -137,25 +271,41 @@ def format_segment_info(segment: Dict[str, Any]) -> str:
137
  elif seg_type == "music":
138
  track = segment.get("track", {})
139
  if track:
140
- return f"""🎵 **Now Playing**
141
 
142
  **{track['title']}**
143
  by {track['artist']}
144
 
145
  Genre: {track['genre']}
146
- Duration: {track['duration']}s
147
 
148
  *{segment.get('commentary', '')}*
149
  """
 
 
 
 
 
 
 
150
  return "🎵 Music Time!"
151
 
152
  elif seg_type == "news":
153
  news_items = segment.get("news_items", [])
154
  news_text = "📰 **News Update**\n\n"
155
- news_text += segment.get("script", "")
 
 
 
 
 
 
 
 
 
156
  news_text += "\n\n**Headlines:**\n"
157
  for item in news_items[:2]:
158
- news_text += f"\n• {item['title']}"
159
  return news_text
160
 
161
  elif seg_type == "podcast":
@@ -185,16 +335,116 @@ def get_segment_text(segment: Dict[str, Any]) -> str:
185
  seg_type = segment["type"]
186
 
187
  if seg_type in ["intro", "outro", "story"]:
188
- return segment.get("content", "")
189
  elif seg_type == "news":
190
- return segment.get("script", "")
 
 
 
 
 
 
191
  elif seg_type == "music":
192
- return segment.get("commentary", "")
193
  elif seg_type == "podcast":
194
- return segment.get("intro", "")
195
 
196
  return ""
197
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
198
  def get_now_playing(segment: Dict[str, Any]) -> str:
199
  """Get now playing text"""
200
  seg_type = segment["type"]
@@ -298,11 +548,18 @@ with gr.Blocks(css=custom_css, title="AI Radio 🎵", theme=gr.themes.Soft()) as
298
  next_btn = gr.Button("⏭️ Next Segment", variant="secondary", size="lg", elem_classes="control-button")
299
  stop_btn = gr.Button("⏹️ Stop", variant="stop", size="lg", elem_classes="control-button")
300
 
 
 
 
 
301
  progress_text = gr.Textbox(label="Progress", value="Ready to start", interactive=False)
302
 
303
  segment_info = gr.Markdown("**Welcome!** Set your preferences and start the radio.", elem_id="segment-display")
304
 
305
- audio_output = gr.Audio(label="🔊 Now Playing", autoplay=True, type="filepath")
 
 
 
306
 
307
  status_text = gr.Textbox(label="Status", value="Ready", interactive=False)
308
 
@@ -310,17 +567,17 @@ with gr.Blocks(css=custom_css, title="AI Radio 🎵", theme=gr.themes.Soft()) as
310
  start_btn.click(
311
  fn=start_radio_stream,
312
  inputs=[],
313
- outputs=[status_text, audio_output, progress_text, now_playing]
314
  ).then(
315
  fn=play_next_segment,
316
  inputs=[],
317
- outputs=[segment_info, audio_output, progress_text, now_playing]
318
  )
319
 
320
  next_btn.click(
321
  fn=play_next_segment,
322
  inputs=[],
323
- outputs=[segment_info, audio_output, progress_text, now_playing]
324
  )
325
 
326
  stop_btn.click(
@@ -328,6 +585,18 @@ with gr.Blocks(css=custom_css, title="AI Radio 🎵", theme=gr.themes.Soft()) as
328
  inputs=[],
329
  outputs=[status_text]
330
  )
 
 
 
 
 
 
 
 
 
 
 
 
331
 
332
  # Tab 2: Preferences
333
  with gr.Tab("⚙️ Your Preferences"):
@@ -370,12 +639,22 @@ with gr.Blocks(css=custom_css, title="AI Radio 🎵", theme=gr.themes.Soft()) as
370
  value=["technology"]
371
  )
372
 
 
 
 
 
 
 
 
 
 
373
  save_pref_btn = gr.Button("💾 Save Preferences", variant="primary", size="lg")
374
  pref_status = gr.Textbox(label="Status", interactive=False)
375
 
376
  save_pref_btn.click(
377
  fn=save_preferences,
378
- inputs=[name_input, genres_input, interests_input, podcast_input, mood_input],
 
379
  outputs=[pref_status]
380
  )
381
 
@@ -458,7 +737,7 @@ with gr.Blocks(css=custom_css, title="AI Radio 🎵", theme=gr.themes.Soft()) as
458
  if __name__ == "__main__":
459
  demo.launch(
460
  server_name="0.0.0.0",
461
- server_port=7860,
462
  share=False
463
  )
464
 
 
10
  from radio_agent import RadioAgent
11
  from tts_service import TTSService
12
  from rag_system import RadioRAGSystem
13
+ from voice_input import VoiceInputService
14
 
15
  # Global state
16
  radio_state = {
 
18
  "current_segment_index": 0,
19
  "planned_show": [],
20
  "user_preferences": {},
21
+ "stop_flag": False,
22
+ "content_filter": {
23
+ "music": True,
24
+ "news": True,
25
+ "podcasts": True,
26
+ "stories": True
27
+ }
28
  }
29
 
30
  # Initialize services
31
  config = get_config()
32
  agent = RadioAgent(config)
33
  tts_service = TTSService(api_key=config.elevenlabs_api_key, voice_id=config.elevenlabs_voice_id)
34
+ voice_input_service = VoiceInputService()
35
 
36
  def save_preferences(name: str, favorite_genres: List[str], interests: List[str],
37
+ podcast_interests: List[str], mood: str,
38
+ music_filter: bool, news_filter: bool, podcast_filter: bool, story_filter: bool) -> str:
39
  """Save user preferences to RAG system"""
40
  preferences = {
41
  "name": name or "Friend",
 
46
  "timestamp": time.time()
47
  }
48
 
49
+ # Update content filter
50
+ radio_state["content_filter"] = {
51
+ "music": music_filter,
52
+ "news": news_filter,
53
+ "podcasts": podcast_filter,
54
+ "stories": story_filter
55
+ }
56
+
57
  radio_state["user_preferences"] = preferences
58
  agent.rag_system.store_user_preferences(preferences)
59
 
 
62
  def start_radio_stream():
63
  """Start the radio stream"""
64
  if not radio_state["user_preferences"]:
65
+ return "⚠️ Please set your preferences first!", None, None, None, ""
66
 
67
  if radio_state["is_playing"]:
68
+ return "📻 Radio is already playing!", None, None, None, ""
69
 
70
+ # Plan the show with content filter
71
  show_plan = agent.plan_radio_show(
72
  user_preferences=radio_state["user_preferences"],
73
+ duration_minutes=30,
74
+ content_filter=radio_state["content_filter"]
75
  )
76
 
77
  radio_state["planned_show"] = show_plan
 
79
  radio_state["is_playing"] = True
80
  radio_state["stop_flag"] = False
81
 
82
+ return "🎵 Starting your personalized radio show...", None, None, None, ""
83
 
84
  def play_next_segment():
85
+ """Play the next segment in the show - returns host audio and music audio separately"""
86
  if not radio_state["is_playing"]:
87
+ return "⏸️ Radio stopped", None, None, None, ""
88
 
89
  if radio_state["stop_flag"]:
90
  radio_state["is_playing"] = False
91
+ return "⏸️ Radio paused", None, None, None, ""
92
 
93
  if radio_state["current_segment_index"] >= len(radio_state["planned_show"]):
94
  radio_state["is_playing"] = False
95
+ return "🎊 Show completed! Hope you enjoyed it.", None, None, None, ""
96
 
97
  # Get current segment
98
  segment = radio_state["planned_show"][radio_state["current_segment_index"]]
 
105
  segment_info = format_segment_info(segment)
106
 
107
  # Generate TTS for host commentary
108
+ host_audio_file = None
109
+ music_player_html = ""
110
+
111
+ if segment["type"] in ["intro", "outro", "story"]:
112
  text = get_segment_text(segment)
113
  if text and tts_service.client:
114
  audio_bytes = tts_service.text_to_speech(text)
115
  if audio_bytes:
116
+ host_audio_file = f"segment_{radio_state['current_segment_index']}.mp3"
117
+ tts_service.save_audio(audio_bytes, host_audio_file)
118
+
119
+ elif segment["type"] == "news":
120
+ # Handle batched news generation
121
+ script_batches = segment.get("script_batches", segment.get("script", []))
122
+ if isinstance(script_batches, list) and script_batches:
123
+ # Generate first batch immediately (user doesn't wait)
124
+ first_batch = script_batches[0]
125
+ if tts_service.client:
126
+ audio_bytes = tts_service.text_to_speech(first_batch)
127
+ if audio_bytes:
128
+ host_audio_file = f"segment_{radio_state['current_segment_index']}_batch_1.mp3"
129
+ tts_service.save_audio(audio_bytes, host_audio_file)
130
+ # Store remaining batches for sequential playback
131
+ segment["remaining_batches"] = script_batches[1:] if len(script_batches) > 1 else []
132
+ else:
133
+ # Fallback for old format
134
+ text = get_segment_text(segment)
135
+ if text and tts_service.client:
136
+ audio_bytes = tts_service.text_to_speech(text)
137
+ if audio_bytes:
138
+ host_audio_file = f"segment_{radio_state['current_segment_index']}.mp3"
139
+ tts_service.save_audio(audio_bytes, host_audio_file)
140
 
141
  elif segment["type"] == "music":
142
+ # For music: generate host commentary, then get music
143
  commentary = segment.get("commentary", "")
144
+
145
+ # Generate host commentary
146
  if commentary and tts_service.client:
147
  audio_bytes = tts_service.text_to_speech(commentary)
148
  if audio_bytes:
149
+ host_audio_file = f"segment_{radio_state['current_segment_index']}_host.mp3"
150
+ tts_service.save_audio(audio_bytes, host_audio_file)
151
+
152
+ # Get music track and create streaming player
153
+ track = segment.get("track", {})
154
+ if track:
155
+ print(f"🎵 Preparing music: {track.get('title', 'Unknown')} by {track.get('artist', 'Unknown')}")
156
+
157
+ # Get streaming URL for YouTube or SoundCloud
158
+ if track.get("source") == "youtube" and track.get("url"):
159
+ try:
160
+ # Extract YouTube ID
161
+ youtube_id = track.get("youtube_id", "")
162
+ if not youtube_id and "v=" in track["url"]:
163
+ youtube_id = track["url"].split("v=")[-1].split("&")[0]
164
+
165
+ if youtube_id:
166
+ # Use YouTube embed player
167
+ # Note: Autoplay may be blocked by browser policies, user can click play
168
+ music_player_html = f"""
169
+ <div style="padding: 1rem; background: #f0f0f0; border-radius: 10px; margin: 1rem 0;">
170
+ <h4 style="margin: 0 0 0.5rem 0;">🎵 {track.get('title', 'Unknown')} - {track.get('artist', 'Unknown')}</h4>
171
+ <div style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden; max-width: 100%; background: #000; border-radius: 8px;">
172
+ <iframe
173
+ style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border: none;"
174
+ src="https://www.youtube.com/embed/{youtube_id}?autoplay=1&rel=0&modestbranding=1&playsinline=1"
175
+ allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
176
+ allowfullscreen
177
+ loading="lazy">
178
+ </iframe>
179
+ </div>
180
+ <p style="margin-top: 0.5rem; font-size: 0.85em; color: #666; margin-bottom: 0;">
181
+ <a href="{track['url']}" target="_blank" style="color: #0066cc; text-decoration: none;">🔗 Open on YouTube</a>
182
+ <span style="margin-left: 1rem; color: #999;">💡 Click ▶️ to play if autoplay is blocked</span>
183
+ </p>
184
+ </div>
185
+ """
186
+ print(f"✅ YouTube player created: {track.get('title', 'Unknown')} (ID: {youtube_id})")
187
+ else:
188
+ # Fallback: Just show link
189
+ music_player_html = f"""
190
+ <div style="padding: 1rem; background: #f0f0f0; border-radius: 10px; margin: 1rem 0;">
191
+ <h4>🎵 {track.get('title', 'Unknown')} - {track.get('artist', 'Unknown')}</h4>
192
+ <p>Click to listen:</p>
193
+ <a href="{track['url']}" target="_blank" style="display: inline-block; padding: 0.5rem 1rem; background: #ff0000; color: white; text-decoration: none; border-radius: 5px;">
194
+ ▶️ Play on YouTube
195
+ </a>
196
+ </div>
197
+ """
198
+ except Exception as e:
199
+ print(f"❌ Error getting music: {e}")
200
+ # Fallback: Just show YouTube link
201
+ music_player_html = f"""
202
+ <div style="padding: 1rem; background: #f0f0f0; border-radius: 10px; margin: 1rem 0;">
203
+ <h4>🎵 {track.get('title', 'Unknown')} - {track.get('artist', 'Unknown')}</h4>
204
+ <p>Click to listen:</p>
205
+ <a href="{track['url']}" target="_blank" style="display: inline-block; padding: 0.5rem 1rem; background: #ff0000; color: white; text-decoration: none; border-radius: 5px;">
206
+ ▶️ Play on YouTube
207
+ </a>
208
+ </div>
209
+ """
210
+ elif track.get("source") == "soundcloud" and track.get("url"):
211
+ # SoundCloud embed player
212
+ try:
213
+ music_player_html = f"""
214
+ <div style="padding: 1rem; background: #f0f0f0; border-radius: 10px; margin: 1rem 0;">
215
+ <h4>🎵 {track.get('title', 'Unknown')} - {track.get('artist', 'Unknown')}</h4>
216
+ <iframe width="100%" height="166" scrolling="no" frameborder="no"
217
+ src="https://w.soundcloud.com/player/?url={track['url']}&color=%23ff5500&auto_play=true&hide_related=false&show_comments=true&show_user=true&show_reposts=false&show_teaser=true"></iframe>
218
+ <p style="margin-top: 0.5rem; font-size: 0.9em; color: #666;">
219
+ <a href="{track['url']}" target="_blank">🔗 Open on SoundCloud</a>
220
+ </p>
221
+ </div>
222
+ """
223
+ print(f"✅ Streaming from SoundCloud: {track.get('title', 'Unknown')}")
224
+ except Exception as e:
225
+ print(f"❌ Error with SoundCloud: {e}")
226
+ music_player_html = f"""
227
+ <div style="padding: 1rem; background: #f0f0f0; border-radius: 10px; margin: 1rem 0;">
228
+ <h4>🎵 {track.get('title', 'Unknown')} - {track.get('artist', 'Unknown')}</h4>
229
+ <a href="{track['url']}" target="_blank" style="display: inline-block; padding: 0.5rem 1rem; background: #ff5500; color: white; text-decoration: none; border-radius: 5px;">
230
+ ▶️ Play on SoundCloud
231
+ </a>
232
+ </div>
233
+ """
234
+ else:
235
+ # Demo track - no actual audio file
236
+ print("ℹ️ Demo track (no audio file available)")
237
+ music_player_html = ""
238
 
239
  elif segment["type"] == "podcast":
240
+ # For podcast: generate intro
241
  intro = segment.get("intro", "")
242
+
243
  if intro and tts_service.client:
244
  audio_bytes = tts_service.text_to_speech(intro)
245
  if audio_bytes:
246
+ host_audio_file = f"segment_{radio_state['current_segment_index']}_host.mp3"
247
+ tts_service.save_audio(audio_bytes, host_audio_file)
248
 
249
  # Progress info
250
  progress = f"Segment {radio_state['current_segment_index']}/{len(radio_state['planned_show'])}"
251
 
252
+ # Return: segment_info, host_audio, music_player_html, progress, now_playing
253
+ return segment_info, host_audio_file, music_player_html, progress, get_now_playing(segment)
254
 
255
  def stop_radio():
256
  """Stop the radio stream"""
 
271
  elif seg_type == "music":
272
  track = segment.get("track", {})
273
  if track:
274
+ music_info = f"""🎵 **Now Playing**
275
 
276
  **{track['title']}**
277
  by {track['artist']}
278
 
279
  Genre: {track['genre']}
280
+ Duration: {track.get('duration', 'Unknown')}s
281
 
282
  *{segment.get('commentary', '')}*
283
  """
284
+ # Add YouTube link if available
285
+ if segment.get("_youtube_url"):
286
+ music_info += f"\n\n🔗 [Listen on YouTube]({segment['_youtube_url']})"
287
+ elif track.get("url") and "youtube" in track.get("url", ""):
288
+ music_info += f"\n\n🔗 [Listen on YouTube]({track['url']})"
289
+
290
+ return music_info
291
  return "🎵 Music Time!"
292
 
293
  elif seg_type == "news":
294
  news_items = segment.get("news_items", [])
295
  news_text = "📰 **News Update**\n\n"
296
+
297
+ # Handle batched script (list) or old format (string)
298
+ script = segment.get("script", segment.get("script_batches", ""))
299
+ if isinstance(script, list):
300
+ # Join all batches for display
301
+ news_text += " ".join(script)
302
+ else:
303
+ # Old format - string
304
+ news_text += str(script) if script else ""
305
+
306
  news_text += "\n\n**Headlines:**\n"
307
  for item in news_items[:2]:
308
+ news_text += f"\n• {item.get('title', 'No title')}"
309
  return news_text
310
 
311
  elif seg_type == "podcast":
 
335
  seg_type = segment["type"]
336
 
337
  if seg_type in ["intro", "outro", "story"]:
338
+ return str(segment.get("content", ""))
339
  elif seg_type == "news":
340
+ # Handle batched news
341
+ script_batches = segment.get("script_batches", segment.get("script", []))
342
+ if isinstance(script_batches, list):
343
+ if script_batches:
344
+ return " ".join(str(batch) for batch in script_batches) # Join all batches
345
+ return ""
346
+ return str(script_batches) if script_batches else "" # Fallback
347
  elif seg_type == "music":
348
+ return str(segment.get("commentary", ""))
349
  elif seg_type == "podcast":
350
+ return str(segment.get("intro", ""))
351
 
352
  return ""
353
 
354
+ def handle_voice_request():
355
+ """Handle voice input request for song"""
356
+ # Check if voice input is available
357
+ if not voice_input_service.available:
358
+ return "⚠️ Voice input is not available. Please install PortAudio and pyaudio.\n\nSee INSTALL_VOICE_INPUT.md for instructions.\n\nYou can still request songs by typing in preferences!", None, ""
359
+
360
+ try:
361
+ # Listen and recognize
362
+ recognized_text = voice_input_service.listen_and_recognize(timeout=5, phrase_time_limit=10)
363
+
364
+ if not recognized_text:
365
+ return "❌ Could not recognize speech. Please try again.", None, ""
366
+
367
+ # Process the request
368
+ song_request = voice_input_service.process_song_request(recognized_text)
369
+ print(f"🎤 Processed request: {song_request}")
370
+
371
+ # Search for music
372
+ tracks = agent.music_server.search_by_request(song_request)
373
+
374
+ if not tracks:
375
+ return f"❌ Could not find music for: '{recognized_text}'. Try saying something like 'play pop music' or 'play a song by [artist name]'!", None, ""
376
+
377
+ # Get the first matching track
378
+ track = tracks[0]
379
+ print(f"🎵 Selected track: {track.get('title', 'Unknown')} by {track.get('artist', 'Unknown')}")
380
+
381
+ # Generate host response
382
+ host_response = f"Great choice! I found '{track['title']}' by {track['artist']}. Let me play that for you!"
383
+
384
+ # Generate TTS for host response
385
+ audio_file = None
386
+ if tts_service.client:
387
+ audio_bytes = tts_service.text_to_speech(host_response)
388
+ if audio_bytes:
389
+ audio_file = f"voice_request_{int(time.time())}.mp3"
390
+ tts_service.save_audio(audio_bytes, audio_file)
391
+
392
+ # Create music player HTML
393
+ music_player_html = ""
394
+ if track.get("source") == "youtube":
395
+ youtube_id = track.get("youtube_id", "")
396
+ if not youtube_id and "v=" in track.get("url", ""):
397
+ youtube_id = track["url"].split("v=")[-1].split("&")[0]
398
+
399
+ if youtube_id:
400
+ # Use YouTube embed player
401
+ music_player_html = f"""
402
+ <div style="padding: 1rem; background: #f0f0f0; border-radius: 10px; margin: 1rem 0;">
403
+ <h4 style="margin: 0 0 0.5rem 0;">🎵 {track.get('title', 'Unknown')} - {track.get('artist', 'Unknown')}</h4>
404
+ <div style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden; max-width: 100%; background: #000; border-radius: 8px;">
405
+ <iframe
406
+ style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border: none;"
407
+ src="https://www.youtube.com/embed/{youtube_id}?autoplay=1&rel=0&modestbranding=1&playsinline=1"
408
+ allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
409
+ allowfullscreen
410
+ loading="lazy">
411
+ </iframe>
412
+ </div>
413
+ <p style="margin-top: 0.5rem; font-size: 0.85em; color: #666; margin-bottom: 0;">
414
+ <a href="{track['url']}" target="_blank" style="color: #0066cc; text-decoration: none;">🔗 Open on YouTube</a>
415
+ <span style="margin-left: 1rem; color: #999;">💡 Click ▶️ to play if autoplay is blocked</span>
416
+ </p>
417
+ </div>
418
+ """
419
+ print(f"✅ YouTube player created for voice request: {track.get('title', 'Unknown')} (ID: {youtube_id})")
420
+ else:
421
+ music_player_html = f"""
422
+ <div style="padding: 1rem; background: #f0f0f0; border-radius: 10px; margin: 1rem 0;">
423
+ <h4>🎵 {track.get('title', 'Unknown')} - {track.get('artist', 'Unknown')}</h4>
424
+ <a href="{track['url']}" target="_blank" style="display: inline-block; padding: 0.5rem 1rem; background: #ff0000; color: white; text-decoration: none; border-radius: 5px;">
425
+ ▶️ Play on YouTube
426
+ </a>
427
+ </div>
428
+ """
429
+ elif track.get("source") == "soundcloud":
430
+ # SoundCloud embed player
431
+ music_player_html = f"""
432
+ <div style="padding: 1rem; background: #f0f0f0; border-radius: 10px; margin: 1rem 0;">
433
+ <h4>🎵 {track.get('title', 'Unknown')} - {track.get('artist', 'Unknown')}</h4>
434
+ <iframe width="100%" height="166" scrolling="no" frameborder="no"
435
+ src="https://w.soundcloud.com/player/?url={track['url']}&color=%23ff5500&auto_play=true&hide_related=false&show_comments=true&show_user=true&show_reposts=false&show_teaser=true"></iframe>
436
+ <p style="margin-top: 0.5rem; font-size: 0.9em; color: #666;">
437
+ <a href="{track['url']}" target="_blank">🔗 Open on SoundCloud</a>
438
+ </p>
439
+ </div>
440
+ """
441
+
442
+ # Add to current show or play immediately
443
+ return f"✅ {host_response}\n\n🎵 Playing: {track['title']} by {track['artist']}", audio_file, music_player_html
444
+
445
+ except Exception as e:
446
+ return f"❌ Error processing voice request: {e}", None, ""
447
+
448
  def get_now_playing(segment: Dict[str, Any]) -> str:
449
  """Get now playing text"""
450
  seg_type = segment["type"]
 
548
  next_btn = gr.Button("⏭️ Next Segment", variant="secondary", size="lg", elem_classes="control-button")
549
  stop_btn = gr.Button("⏹️ Stop", variant="stop", size="lg", elem_classes="control-button")
550
 
551
+ with gr.Row():
552
+ voice_btn = gr.Button("🎤 Ask for a Song", variant="primary", size="lg", elem_classes="control-button")
553
+ voice_status = gr.Textbox(label="Voice Request", value="Click to request a song by voice", interactive=False)
554
+
555
  progress_text = gr.Textbox(label="Progress", value="Ready to start", interactive=False)
556
 
557
  segment_info = gr.Markdown("**Welcome!** Set your preferences and start the radio.", elem_id="segment-display")
558
 
559
+ gr.Markdown("**💡 Tip:** Host speech plays first, then music/podcasts will stream automatically!")
560
+
561
+ audio_output = gr.Audio(label="🔊 Host Speech", autoplay=True, type="filepath")
562
+ music_player = gr.HTML(label="🎵 Music/Podcast Player (streaming)")
563
 
564
  status_text = gr.Textbox(label="Status", value="Ready", interactive=False)
565
 
 
567
  start_btn.click(
568
  fn=start_radio_stream,
569
  inputs=[],
570
+ outputs=[status_text, audio_output, music_player, progress_text, now_playing]
571
  ).then(
572
  fn=play_next_segment,
573
  inputs=[],
574
+ outputs=[segment_info, audio_output, music_player, progress_text, now_playing]
575
  )
576
 
577
  next_btn.click(
578
  fn=play_next_segment,
579
  inputs=[],
580
+ outputs=[segment_info, audio_output, music_player, progress_text, now_playing]
581
  )
582
 
583
  stop_btn.click(
 
585
  inputs=[],
586
  outputs=[status_text]
587
  )
588
+
589
+ # Voice input button
590
+ voice_btn.click(
591
+ fn=handle_voice_request,
592
+ inputs=[],
593
+ outputs=[voice_status, audio_output, music_player]
594
+ ).then(
595
+ # After voice request, update status
596
+ fn=lambda status, audio, player: (status, audio, player),
597
+ inputs=[voice_status, audio_output, music_player],
598
+ outputs=[voice_status, audio_output, music_player]
599
+ )
600
 
601
  # Tab 2: Preferences
602
  with gr.Tab("⚙️ Your Preferences"):
 
639
  value=["technology"]
640
  )
641
 
642
+ gr.Markdown("### 🎛️ Content Filter")
643
+ gr.Markdown("Choose what types of content you want to hear:")
644
+
645
+ with gr.Row():
646
+ music_filter = gr.Checkbox(label="🎵 Music", value=True)
647
+ news_filter = gr.Checkbox(label="📰 News", value=True)
648
+ podcast_filter = gr.Checkbox(label="🎙️ Podcasts", value=True)
649
+ story_filter = gr.Checkbox(label="📖 AI Stories", value=True)
650
+
651
  save_pref_btn = gr.Button("💾 Save Preferences", variant="primary", size="lg")
652
  pref_status = gr.Textbox(label="Status", interactive=False)
653
 
654
  save_pref_btn.click(
655
  fn=save_preferences,
656
+ inputs=[name_input, genres_input, interests_input, podcast_input, mood_input,
657
+ music_filter, news_filter, podcast_filter, story_filter],
658
  outputs=[pref_status]
659
  )
660
 
 
737
  if __name__ == "__main__":
738
  demo.launch(
739
  server_name="0.0.0.0",
740
+ server_port=7861,
741
  share=False
742
  )
743
 
config.py CHANGED
@@ -10,6 +10,8 @@ class RadioConfig(BaseModel):
10
  google_api_key: str = "AIzaSyB5F9P0oDZ6fgW8GgADfwnwcg-GkHrdo74"
11
  llamaindex_api_key: str = "llx-WRsj0iehk2ZlSlNIenOLyyhO9X1yFT4CmJXpl0qk6hapFi01"
12
  nebius_api_key: str = "v1.CmQKHHN0YXRpY2tleS1lMDB0eTkxeTdwY3lxNDk5OWcSIXNlcnZpY2VhY2NvdW50LWUwMGowemtmZWpqc2E3ZHF3aDIMCKb4oskGENS9j8MBOgwIpfu6lAcQgOqAhwNAAloDZTAw.AAAAAAAAAAGEI_L5sJCQ7XR93nSzvXCPO-J3-gHjqPiRqrvkrMLeDtd-70zGWB1-c8yovnX-q7yEc1dHOnA2L8FUa3Le6X8D"
 
 
13
 
14
  # Voice Settings
15
  elevenlabs_voice_id: str = "21m00Tcm4TlvDq8ikWAM" # Default voice (Rachel)
 
10
  google_api_key: str = "AIzaSyB5F9P0oDZ6fgW8GgADfwnwcg-GkHrdo74"
11
  llamaindex_api_key: str = "llx-WRsj0iehk2ZlSlNIenOLyyhO9X1yFT4CmJXpl0qk6hapFi01"
12
  nebius_api_key: str = "v1.CmQKHHN0YXRpY2tleS1lMDB0eTkxeTdwY3lxNDk5OWcSIXNlcnZpY2VhY2NvdW50LWUwMGowemtmZWpqc2E3ZHF3aDIMCKb4oskGENS9j8MBOgwIpfu6lAcQgOqAhwNAAloDZTAw.AAAAAAAAAAGEI_L5sJCQ7XR93nSzvXCPO-J3-gHjqPiRqrvkrMLeDtd-70zGWB1-c8yovnX-q7yEc1dHOnA2L8FUa3Le6X8D"
13
+ nebius_api_base: str = "https://api.nebius.ai/v1" # Nebius OpenAI-compatible endpoint
14
+ nebius_model: str = "gpt-oss-120b" # GPT-OSS-120B model from Nebius
15
 
16
  # Voice Settings
17
  elevenlabs_voice_id: str = "21m00Tcm4TlvDq8ikWAM" # Default voice (Rachel)
mcp_servers/music_server.py CHANGED
@@ -1,7 +1,9 @@
1
  """MCP Server for Music Recommendations"""
2
  import json
3
  import requests
4
- from typing import List, Dict, Any
 
 
5
  from dataclasses import dataclass
6
 
7
  @dataclass
@@ -19,11 +21,216 @@ class MusicMCPServer:
19
  def __init__(self):
20
  self.name = "music_server"
21
  self.description = "Provides music recommendations and free music tracks"
 
 
22
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
23
  def search_free_music(self, genre: str = "pop", mood: str = "happy", limit: int = 5) -> List[Dict[str, Any]]:
24
  """
25
  Search for free music tracks based on genre and mood
26
- Uses Free Music Archive API (simulated for demo)
27
 
28
  Args:
29
  genre: Music genre (pop, rock, jazz, classical, electronic, etc.)
@@ -33,33 +240,83 @@ class MusicMCPServer:
33
  Returns:
34
  List of track dictionaries
35
  """
36
- # In production, integrate with actual free music APIs like:
37
- # - Free Music Archive (FMA)
38
- # - Jamendo API
39
- # - ccMixter
40
- # For now, return demo tracks
 
41
 
 
 
 
 
 
 
42
  demo_tracks = {
43
  "pop": [
44
- {"title": "Sunshine Day", "artist": "Happy Vibes", "url": "demo_track_1.mp3", "duration": 180, "genre": "pop"},
45
- {"title": "Feel Good", "artist": "The Cheerful", "url": "demo_track_2.mp3", "duration": 200, "genre": "pop"},
46
- {"title": "Summer Love", "artist": "Pop Stars", "url": "demo_track_3.mp3", "duration": 195, "genre": "pop"},
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
47
  ],
48
  "rock": [
49
- {"title": "Thunder Road", "artist": "Rock Squad", "url": "demo_track_4.mp3", "duration": 220, "genre": "rock"},
50
- {"title": "Electric Soul", "artist": "The Rockers", "url": "demo_track_5.mp3", "duration": 210, "genre": "rock"},
 
 
 
 
 
 
 
51
  ],
52
  "jazz": [
53
- {"title": "Midnight Blue", "artist": "Jazz Ensemble", "url": "demo_track_6.mp3", "duration": 240, "genre": "jazz"},
54
- {"title": "Smooth Sax", "artist": "Cool Jazz Band", "url": "demo_track_7.mp3", "duration": 260, "genre": "jazz"},
 
 
 
 
 
 
 
55
  ],
56
  "classical": [
57
- {"title": "Morning Sonata", "artist": "Classical Orchestra", "url": "demo_track_8.mp3", "duration": 300, "genre": "classical"},
58
- {"title": "Peaceful Symphony", "artist": "Chamber Ensemble", "url": "demo_track_9.mp3", "duration": 280, "genre": "classical"},
 
 
 
 
 
 
 
59
  ],
60
  "electronic": [
61
- {"title": "Digital Dreams", "artist": "Synth Wave", "url": "demo_track_10.mp3", "duration": 190, "genre": "electronic"},
62
- {"title": "Neon Lights", "artist": "Electro Beats", "url": "demo_track_11.mp3", "duration": 185, "genre": "electronic"},
 
 
 
 
 
 
 
63
  ]
64
  }
65
 
@@ -68,6 +325,81 @@ class MusicMCPServer:
68
 
69
  return tracks[:limit]
70
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
71
  def get_personalized_playlist(self, user_preferences: Dict[str, Any]) -> List[Dict[str, Any]]:
72
  """
73
  Generate personalized playlist based on user preferences
 
1
  """MCP Server for Music Recommendations"""
2
  import json
3
  import requests
4
+ import yt_dlp
5
+ import os
6
+ from typing import List, Dict, Any, Optional
7
  from dataclasses import dataclass
8
 
9
  @dataclass
 
21
  def __init__(self):
22
  self.name = "music_server"
23
  self.description = "Provides music recommendations and free music tracks"
24
+ self.cache_dir = "music_cache"
25
+ os.makedirs(self.cache_dir, exist_ok=True)
26
 
27
+ def search_youtube_music(self, query: str, limit: int = 5) -> List[Dict[str, Any]]:
28
+ """
29
+ Search for free music on YouTube
30
+
31
+ Args:
32
+ query: Search query (e.g., "pop music", "jazz instrumental", "song name")
33
+ limit: Number of results
34
+
35
+ Returns:
36
+ List of track dictionaries with YouTube URLs
37
+ """
38
+ tracks = []
39
+ try:
40
+ # First try with extract_flat=False to get full info
41
+ ydl_opts = {
42
+ 'quiet': True,
43
+ 'no_warnings': True,
44
+ 'extract_flat': False, # Get full info for better results
45
+ 'default_search': 'ytsearch',
46
+ 'format': 'bestaudio/best',
47
+ }
48
+
49
+ with yt_dlp.YoutubeDL(ydl_opts) as ydl:
50
+ # Don't add "music" if query already contains it or is specific
51
+ if "music" not in query.lower() and len(query.split()) < 4:
52
+ search_query = f"ytsearch{limit}:{query} music"
53
+ else:
54
+ search_query = f"ytsearch{limit}:{query}"
55
+
56
+ print(f"🔍 YouTube search query: '{search_query}'")
57
+ results = ydl.extract_info(search_query, download=False)
58
+
59
+ # Handle different result formats from yt-dlp
60
+ entries = None
61
+
62
+ if isinstance(results, dict):
63
+ # Standard format with entries list
64
+ if 'entries' in results:
65
+ entries = results['entries']
66
+ # Sometimes the dict itself contains video info
67
+ elif 'id' in results:
68
+ entries = [results]
69
+ elif isinstance(results, list):
70
+ entries = results
71
+
72
+ if entries:
73
+ for entry in entries[:limit]:
74
+ if entry and isinstance(entry, dict):
75
+ video_id = entry.get('id') or entry.get('url', '').split('v=')[-1].split('&')[0] if 'v=' in str(entry.get('url', '')) else None
76
+ if video_id:
77
+ track = {
78
+ "title": entry.get('title', 'Unknown'),
79
+ "artist": entry.get('uploader', entry.get('channel', entry.get('channel_name', 'Unknown Artist'))),
80
+ "url": f"https://www.youtube.com/watch?v={video_id}",
81
+ "youtube_id": video_id,
82
+ "duration": entry.get('duration', 0),
83
+ "genre": query.split()[0] if query else "unknown",
84
+ "source": "youtube"
85
+ }
86
+ tracks.append(track)
87
+ print(f" ✓ Found: {track['title']} by {track['artist']}")
88
+ else:
89
+ # Fallback: try extract_flat=True
90
+ print(" ⚠️ Trying flat extraction...")
91
+ ydl_opts_flat = {
92
+ 'quiet': True,
93
+ 'no_warnings': True,
94
+ 'extract_flat': True,
95
+ 'default_search': 'ytsearch',
96
+ }
97
+ with yt_dlp.YoutubeDL(ydl_opts_flat) as ydl_flat:
98
+ results_flat = ydl_flat.extract_info(f"ytsearch{limit}:{query}", download=False)
99
+ if isinstance(results_flat, dict) and 'entries' in results_flat:
100
+ for entry in results_flat['entries'][:limit]:
101
+ if entry and isinstance(entry, dict) and entry.get('id'):
102
+ track = {
103
+ "title": entry.get('title', 'Unknown'),
104
+ "artist": entry.get('uploader', 'Unknown Artist'),
105
+ "url": f"https://www.youtube.com/watch?v={entry.get('id', '')}",
106
+ "youtube_id": entry.get('id', ''),
107
+ "duration": entry.get('duration', 0),
108
+ "genre": query.split()[0] if query else "unknown",
109
+ "source": "youtube"
110
+ }
111
+ tracks.append(track)
112
+ print(f" ✓ Found (flat): {track['title']} by {track['artist']}")
113
+ except Exception as e:
114
+ print(f"❌ Error searching YouTube: {e}")
115
+ import traceback
116
+ traceback.print_exc()
117
+
118
+ return tracks
119
+
120
+ def search_soundcloud_music(self, query: str, limit: int = 5) -> List[Dict[str, Any]]:
121
+ """
122
+ Search for free music on SoundCloud
123
+
124
+ Args:
125
+ query: Search query (e.g., "pop music", "jazz instrumental")
126
+ limit: Number of results
127
+
128
+ Returns:
129
+ List of track dictionaries with SoundCloud URLs
130
+ """
131
+ tracks = []
132
+ try:
133
+ ydl_opts = {
134
+ 'quiet': True,
135
+ 'no_warnings': True,
136
+ 'extract_flat': True,
137
+ 'default_search': 'scsearch',
138
+ 'format': 'bestaudio/best',
139
+ }
140
+
141
+ with yt_dlp.YoutubeDL(ydl_opts) as ydl:
142
+ search_query = f"{query} music"
143
+ results = ydl.extract_info(search_query, download=False)
144
+
145
+ if 'entries' in results:
146
+ for entry in results['entries'][:limit]:
147
+ if entry:
148
+ track = {
149
+ "title": entry.get('title', 'Unknown'),
150
+ "artist": entry.get('uploader', 'Unknown Artist'),
151
+ "url": entry.get('url', entry.get('webpage_url', '')),
152
+ "duration": entry.get('duration', 0),
153
+ "genre": query.split()[0] if query else "unknown",
154
+ "source": "soundcloud"
155
+ }
156
+ tracks.append(track)
157
+ except Exception as e:
158
+ print(f"Error searching SoundCloud: {e}")
159
+
160
+ return tracks
161
+
162
+ def get_audio_url(self, youtube_url: str) -> Optional[str]:
163
+ """
164
+ Get direct audio URL from YouTube video
165
+
166
+ Args:
167
+ youtube_url: YouTube video URL
168
+
169
+ Returns:
170
+ Direct audio URL or None
171
+ """
172
+ try:
173
+ ydl_opts = {
174
+ 'format': 'bestaudio/best',
175
+ 'quiet': True,
176
+ 'no_warnings': True,
177
+ }
178
+
179
+ with yt_dlp.YoutubeDL(ydl_opts) as ydl:
180
+ info = ydl.extract_info(youtube_url, download=False)
181
+ if 'url' in info:
182
+ return info['url']
183
+ except Exception as e:
184
+ print(f"Error getting audio URL: {e}")
185
+
186
+ return None
187
+
188
+ def download_audio(self, youtube_url: str, output_path: str) -> Optional[str]:
189
+ """
190
+ Download audio from YouTube to local file
191
+
192
+ Args:
193
+ youtube_url: YouTube video URL
194
+ output_path: Path to save audio file (without extension)
195
+
196
+ Returns:
197
+ Path to downloaded file or None
198
+ """
199
+ try:
200
+ # Ensure output path doesn't have extension (yt-dlp adds it)
201
+ if output_path.endswith('.mp3'):
202
+ output_path = output_path[:-4]
203
+
204
+ ydl_opts = {
205
+ 'format': 'bestaudio/best',
206
+ 'outtmpl': output_path + '.%(ext)s',
207
+ 'postprocessors': [{
208
+ 'key': 'FFmpegExtractAudio',
209
+ 'preferredcodec': 'mp3',
210
+ 'preferredquality': '192',
211
+ }],
212
+ 'quiet': True,
213
+ 'no_warnings': True,
214
+ }
215
+
216
+ with yt_dlp.YoutubeDL(ydl_opts) as ydl:
217
+ ydl.download([youtube_url])
218
+ # Check for downloaded file
219
+ if os.path.exists(output_path + '.mp3'):
220
+ return output_path + '.mp3'
221
+ # Sometimes it might be .m4a or other format
222
+ for ext in ['.mp3', '.m4a', '.webm', '.ogg']:
223
+ if os.path.exists(output_path + ext):
224
+ return output_path + ext
225
+ except Exception as e:
226
+ print(f"Error downloading audio: {e}")
227
+
228
+ return None
229
+
230
  def search_free_music(self, genre: str = "pop", mood: str = "happy", limit: int = 5) -> List[Dict[str, Any]]:
231
  """
232
  Search for free music tracks based on genre and mood
233
+ Uses YouTube and SoundCloud for free music
234
 
235
  Args:
236
  genre: Music genre (pop, rock, jazz, classical, electronic, etc.)
 
240
  Returns:
241
  List of track dictionaries
242
  """
243
+ # Try YouTube first
244
+ query = f"{genre} {mood}"
245
+ youtube_tracks = self.search_youtube_music(query, limit=limit)
246
+
247
+ if youtube_tracks:
248
+ return youtube_tracks
249
 
250
+ # Try SoundCloud as fallback
251
+ soundcloud_tracks = self.search_soundcloud_music(query, limit=limit)
252
+ if soundcloud_tracks:
253
+ return soundcloud_tracks
254
+
255
+ # Fallback to demo tracks with real YouTube URLs (free music streams)
256
  demo_tracks = {
257
  "pop": [
258
+ {
259
+ "title": "Lofi Hip Hop Radio",
260
+ "artist": "ChilledCow",
261
+ "url": "https://www.youtube.com/watch?v=jfKfPfyJRdk",
262
+ "youtube_id": "jfKfPfyJRdk",
263
+ "duration": 0, # Live stream
264
+ "genre": "pop",
265
+ "source": "youtube"
266
+ },
267
+ {
268
+ "title": "Synthwave Radio",
269
+ "artist": "Free Music",
270
+ "url": "https://www.youtube.com/watch?v=4xDzrJKXOOY",
271
+ "youtube_id": "4xDzrJKXOOY",
272
+ "duration": 0,
273
+ "genre": "pop",
274
+ "source": "youtube"
275
+ },
276
  ],
277
  "rock": [
278
+ {
279
+ "title": "Rock Music Stream",
280
+ "artist": "Free Music",
281
+ "url": "https://www.youtube.com/watch?v=jfKfPfyJRdk",
282
+ "youtube_id": "jfKfPfyJRdk",
283
+ "duration": 0,
284
+ "genre": "rock",
285
+ "source": "youtube"
286
+ },
287
  ],
288
  "jazz": [
289
+ {
290
+ "title": "Jazz Music",
291
+ "artist": "Free Music",
292
+ "url": "https://www.youtube.com/watch?v=jfKfPfyJRdk",
293
+ "youtube_id": "jfKfPfyJRdk",
294
+ "duration": 0,
295
+ "genre": "jazz",
296
+ "source": "youtube"
297
+ },
298
  ],
299
  "classical": [
300
+ {
301
+ "title": "Classical Music",
302
+ "artist": "Free Music",
303
+ "url": "https://www.youtube.com/watch?v=jfKfPfyJRdk",
304
+ "youtube_id": "jfKfPfyJRdk",
305
+ "duration": 0,
306
+ "genre": "classical",
307
+ "source": "youtube"
308
+ },
309
  ],
310
  "electronic": [
311
+ {
312
+ "title": "Electronic Beats",
313
+ "artist": "Free Music",
314
+ "url": "https://www.youtube.com/watch?v=jfKfPfyJRdk",
315
+ "youtube_id": "jfKfPfyJRdk",
316
+ "duration": 0,
317
+ "genre": "electronic",
318
+ "source": "youtube"
319
+ },
320
  ]
321
  }
322
 
 
325
 
326
  return tracks[:limit]
327
 
328
+ def search_by_request(self, song_request: Dict[str, Any]) -> List[Dict[str, Any]]:
329
+ """
330
+ Search for music based on voice request
331
+
332
+ Args:
333
+ song_request: Dictionary with song request details
334
+
335
+ Returns:
336
+ List of matching tracks
337
+ """
338
+ # Build search query from request
339
+ # Prefer original text, but clean it up
340
+ if song_request.get("original_text"):
341
+ original = song_request["original_text"]
342
+ # Remove common filler words but keep the core query
343
+ filler_words = ["play", "put on", "listen to", "want to hear", "i want to", "can you", "please"]
344
+ query = original.lower()
345
+ for filler in filler_words:
346
+ query = query.replace(filler, "").strip()
347
+ query = " ".join(query.split()) # Clean up spaces
348
+ # If query is too short or unclear, use original
349
+ if len(query.split()) < 2:
350
+ query = original
351
+ else:
352
+ # Build query from parts
353
+ query_parts = []
354
+ if song_request.get("song") and len(song_request["song"].split()) > 1:
355
+ query_parts.append(song_request["song"])
356
+ elif song_request.get("song"):
357
+ # Single word song - add genre or use as-is
358
+ if song_request.get("genre"):
359
+ query_parts.append(song_request["genre"])
360
+ else:
361
+ query_parts.append(song_request["song"])
362
+ if song_request.get("artist"):
363
+ query_parts.append(song_request["artist"])
364
+ if song_request.get("genre") and song_request["genre"] not in " ".join(query_parts):
365
+ query_parts.append(song_request["genre"])
366
+
367
+ query = " ".join(query_parts) if query_parts else "music"
368
+
369
+ print(f"🔍 Searching for music: '{query}'")
370
+
371
+ # Try YouTube first
372
+ tracks = self.search_youtube_music(query, limit=5)
373
+
374
+ if tracks:
375
+ print(f"✅ Found {len(tracks)} tracks on YouTube")
376
+ return tracks
377
+
378
+ # If YouTube fails, try SoundCloud
379
+ print("⚠️ YouTube search failed, trying SoundCloud...")
380
+ tracks = self.search_soundcloud_music(query, limit=5)
381
+
382
+ if tracks:
383
+ print(f"✅ Found {len(tracks)} tracks on SoundCloud")
384
+ return tracks
385
+
386
+ # If both fail, try a simpler query
387
+ print("⚠️ Both searches failed, trying simplified query...")
388
+ if song_request.get("song"):
389
+ simple_query = song_request["song"]
390
+ tracks = self.search_youtube_music(simple_query, limit=5)
391
+ if tracks:
392
+ return tracks
393
+
394
+ # Last resort: search by genre or mood
395
+ if song_request.get("genre"):
396
+ tracks = self.search_free_music(genre=song_request["genre"], limit=5)
397
+ if tracks:
398
+ return tracks
399
+
400
+ print("❌ No tracks found")
401
+ return []
402
+
403
  def get_personalized_playlist(self, user_preferences: Dict[str, Any]) -> List[Dict[str, Any]]:
404
  """
405
  Generate personalized playlist based on user preferences
mcp_servers/news_server.py CHANGED
@@ -3,6 +3,7 @@ import requests
3
  import feedparser
4
  from typing import List, Dict, Any
5
  from datetime import datetime
 
6
 
7
  class NewsMCPServer:
8
  """MCP Server for fetching and curating news"""
@@ -11,30 +12,40 @@ class NewsMCPServer:
11
  self.name = "news_server"
12
  self.description = "Fetches latest news from various sources"
13
 
14
- # Free RSS feeds
15
  self.news_feeds = {
16
  "technology": [
17
- "https://feeds.bbci.co.uk/news/technology/rss.xml",
18
- "https://www.theverge.com/rss/index.xml"
 
19
  ],
20
  "world": [
21
- "https://feeds.bbci.co.uk/news/world/rss.xml",
22
- "https://rss.nytimes.com/services/xml/rss/nyt/World.xml"
 
23
  ],
24
  "business": [
25
- "https://feeds.bbci.co.uk/news/business/rss.xml",
 
26
  ],
27
  "entertainment": [
28
- "https://feeds.bbci.co.uk/news/entertainment_and_arts/rss.xml",
 
29
  ],
30
  "science": [
31
- "https://feeds.bbci.co.uk/news/science_and_environment/rss.xml",
 
32
  ]
33
  }
 
 
 
 
 
34
 
35
  def fetch_news(self, category: str = "world", limit: int = 5) -> List[Dict[str, Any]]:
36
  """
37
- Fetch latest news from RSS feeds
38
 
39
  Args:
40
  category: News category (technology, world, business, entertainment, science)
@@ -43,31 +54,67 @@ class NewsMCPServer:
43
  Returns:
44
  List of news items
45
  """
46
- try:
47
- feeds = self.news_feeds.get(category, self.news_feeds["world"])
48
- all_news = []
49
-
50
- for feed_url in feeds[:2]: # Use first 2 feeds
51
- try:
52
- feed = feedparser.parse(feed_url)
53
- for entry in feed.entries[:limit]:
54
- news_item = {
55
- "title": entry.get("title", ""),
56
- "summary": entry.get("summary", entry.get("description", ""))[:200],
57
- "link": entry.get("link", ""),
58
- "published": entry.get("published", ""),
59
- "category": category
60
- }
61
- all_news.append(news_item)
62
- except Exception as e:
63
- print(f"Error fetching from {feed_url}: {e}")
64
  continue
65
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
66
  return all_news[:limit]
67
 
68
- except Exception as e:
69
- print(f"Error fetching news: {e}")
70
- return self._get_demo_news(category, limit)
71
 
72
  def _get_demo_news(self, category: str, limit: int) -> List[Dict[str, Any]]:
73
  """Return demo news items when RSS feeds fail"""
 
3
  import feedparser
4
  from typing import List, Dict, Any
5
  from datetime import datetime
6
+ import time
7
 
8
  class NewsMCPServer:
9
  """MCP Server for fetching and curating news"""
 
12
  self.name = "news_server"
13
  self.description = "Fetches latest news from various sources"
14
 
15
+ # More reliable RSS feeds with fallbacks
16
  self.news_feeds = {
17
  "technology": [
18
+ "https://rss.cnn.com/rss/edition.rss", # CNN Tech
19
+ "https://feeds.feedburner.com/oreilly/radar", # O'Reilly Radar
20
+ "https://www.wired.com/feed/rss" # Wired
21
  ],
22
  "world": [
23
+ "https://rss.cnn.com/rss/edition.rss", # CNN World
24
+ "https://feeds.reuters.com/reuters/topNews", # Reuters Top News
25
+ "https://rss.nytimes.com/services/xml/rss/nyt/World.xml" # NYT World
26
  ],
27
  "business": [
28
+ "https://feeds.reuters.com/reuters/businessNews", # Reuters Business
29
+ "https://rss.cnn.com/rss/money_latest.rss", # CNN Money
30
  ],
31
  "entertainment": [
32
+ "https://rss.cnn.com/rss/edition_entertainment.rss", # CNN Entertainment
33
+ "https://www.rollingstone.com/feed", # Rolling Stone
34
  ],
35
  "science": [
36
+ "https://rss.cnn.com/rss/edition_space.rss", # CNN Science
37
+ "https://feeds.feedburner.com/sciencealert-latestnews", # Science Alert
38
  ]
39
  }
40
+
41
+ # NewsAPI.org (free tier - 100 requests/day)
42
+ # You can get a free API key from https://newsapi.org/
43
+ self.newsapi_key = None # Optional - can be added later
44
+ self.newsapi_base = "https://newsapi.org/v2"
45
 
46
  def fetch_news(self, category: str = "world", limit: int = 5) -> List[Dict[str, Any]]:
47
  """
48
+ Fetch latest news from RSS feeds with better error handling
49
 
50
  Args:
51
  category: News category (technology, world, business, entertainment, science)
 
54
  Returns:
55
  List of news items
56
  """
57
+ all_news = []
58
+
59
+ # Try RSS feeds first
60
+ feeds = self.news_feeds.get(category, self.news_feeds["world"])
61
+
62
+ for feed_url in feeds:
63
+ try:
64
+ # Add timeout and headers for better reliability
65
+ headers = {
66
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
67
+ }
68
+
69
+ # Parse feed with timeout
70
+ feed = feedparser.parse(feed_url)
71
+
72
+ # Check if feed is valid
73
+ if not feed.entries:
 
74
  continue
75
+
76
+ for entry in feed.entries[:limit]:
77
+ # Safely extract fields
78
+ title = str(entry.get("title", "")).strip()
79
+ summary = str(entry.get("summary", entry.get("description", ""))).strip()
80
+ link = str(entry.get("link", "")).strip()
81
+ published = str(entry.get("published", entry.get("updated", ""))).strip()
82
+
83
+ if not title: # Skip if no title
84
+ continue
85
+
86
+ # Truncate summary safely
87
+ if summary:
88
+ summary = summary[:200] + "..." if len(summary) > 200 else summary
89
+ else:
90
+ summary = "No summary available."
91
+
92
+ news_item = {
93
+ "title": title,
94
+ "summary": summary,
95
+ "link": link if link else "#",
96
+ "published": published if published else datetime.now().strftime("%Y-%m-%d"),
97
+ "category": category
98
+ }
99
+ all_news.append(news_item)
100
+
101
+ if len(all_news) >= limit:
102
+ break
103
+
104
+ if len(all_news) >= limit:
105
+ break
106
+
107
+ except Exception as e:
108
+ print(f"Error fetching from {feed_url}: {e}")
109
+ continue
110
+
111
+ # If we got some news, return it
112
+ if all_news:
113
  return all_news[:limit]
114
 
115
+ # Fallback to demo news if all feeds fail
116
+ print(f"All feeds failed for {category}, using demo news")
117
+ return self._get_demo_news(category, limit)
118
 
119
  def _get_demo_news(self, category: str, limit: int) -> List[Dict[str, Any]]:
120
  """Return demo news items when RSS feeds fail"""
radio_agent.py CHANGED
@@ -3,7 +3,8 @@ import json
3
  import random
4
  from typing import Dict, Any, List, Generator
5
  from datetime import datetime
6
- import google.generativeai as genai
 
7
 
8
  from mcp_servers.music_server import MusicMCPServer
9
  from mcp_servers.news_server import NewsMCPServer
@@ -16,27 +17,47 @@ class RadioAgent:
16
  def __init__(self, config):
17
  self.config = config
18
 
19
- # Initialize Gemini LLM
20
- if config.google_api_key:
21
- genai.configure(api_key=config.google_api_key)
22
- self.model = genai.GenerativeModel('gemini-pro')
23
- else:
24
- self.model = None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
25
 
26
  # Initialize MCP Servers
27
  self.music_server = MusicMCPServer()
28
  self.news_server = NewsMCPServer()
29
  self.podcast_server = PodcastMCPServer()
30
 
31
- # Initialize RAG System
32
- self.rag_system = RadioRAGSystem(config.google_api_key)
33
 
34
  # Agent state
35
  self.is_streaming = False
36
  self.current_segment = None
37
  self.segment_history = []
38
 
39
- def plan_radio_show(self, user_preferences: Dict[str, Any], duration_minutes: int = 30) -> List[Dict[str, Any]]:
 
40
  """
41
  Plan a personalized radio show based on user preferences
42
  This demonstrates autonomous planning behavior
@@ -44,35 +65,65 @@ class RadioAgent:
44
  Args:
45
  user_preferences: User's preferences and mood
46
  duration_minutes: Total duration of the show
 
47
 
48
  Returns:
49
  List of planned segments
50
  """
51
  segments = []
52
 
 
 
 
 
 
 
 
 
 
53
  # Get user preferences from RAG
54
  stored_prefs = self.rag_system.get_user_preferences()
55
  merged_prefs = {**stored_prefs, **user_preferences}
56
 
57
- # Calculate segment distribution
58
  total_segments = max(5, duration_minutes // 5)
59
 
60
- music_count = int(total_segments * self.config.music_ratio)
61
- news_count = int(total_segments * self.config.news_ratio)
62
- podcast_count = int(total_segments * self.config.podcast_ratio)
63
- story_count = max(1, int(total_segments * self.config.story_ratio))
64
 
65
  # Balance to total
66
- remaining = total_segments - (music_count + news_count + podcast_count + story_count)
67
- music_count += remaining
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
68
 
69
  # Create segment plan
70
- segment_types = (
71
- ['music'] * music_count +
72
- ['news'] * news_count +
73
- ['podcast'] * podcast_count +
74
- ['story'] * story_count
75
- )
 
 
 
76
 
77
  # Shuffle for variety
78
  random.shuffle(segment_types)
@@ -105,40 +156,73 @@ class RadioAgent:
105
  return segments
106
 
107
  def _generate_intro(self, preferences: Dict[str, Any]) -> str:
108
- """Generate personalized radio intro"""
109
  mood = preferences.get('mood', 'happy')
110
  name = preferences.get('name', 'friend')
111
  time_of_day = self._get_time_of_day()
 
112
 
113
- if self.model:
114
  try:
115
- prompt = f"""You are a charismatic radio host. Create a warm, engaging {time_of_day} greeting
116
- for {name}. The listener is feeling {mood}. Keep it energetic and personal, 2-3 sentences.
117
- Make them excited to listen!"""
118
 
119
- response = self.model.generate_content(prompt)
120
- return response.text
 
 
 
 
 
 
 
 
 
 
 
 
121
  except Exception as e:
122
  print(f"Error generating intro: {e}")
123
 
 
 
 
 
 
 
 
 
124
  # Fallback intro
125
- return f"Good {time_of_day}, {name}! Welcome to your personal AI Radio station. We've got an amazing show lined up for you today!"
126
 
127
  def _generate_outro(self, preferences: Dict[str, Any]) -> str:
128
  """Generate personalized radio outro"""
129
  name = preferences.get('name', 'friend')
130
 
131
- if self.model:
132
  try:
133
  prompt = f"""You are a charismatic radio host wrapping up a show.
134
  Create a warm, friendly goodbye message for {name}.
135
  Thank them for listening and invite them back. Keep it 2 sentences."""
136
 
137
- response = self.model.generate_content(prompt)
138
- return response.text
 
 
 
 
 
139
  except Exception as e:
140
  print(f"Error generating outro: {e}")
141
 
 
 
 
 
 
 
 
 
142
  return f"That's all for now, {name}! Thanks for tuning in to AI Radio. Come back soon for more personalized content!"
143
 
144
  def _plan_music_segment(self, preferences: Dict[str, Any]) -> Dict[str, Any]:
@@ -166,21 +250,22 @@ class RadioAgent:
166
  }
167
 
168
  def _plan_news_segment(self, preferences: Dict[str, Any]) -> Dict[str, Any]:
169
- """Plan a news segment using News MCP Server"""
170
  interests = preferences.get('interests', ['world'])
171
  category = random.choice(interests)
172
 
173
  # Use MCP server to get news
174
- news_items = self.news_server.fetch_news(category=category, limit=2)
175
 
176
- # Generate news script
177
- script = self._generate_news_script(news_items, preferences)
178
 
179
  return {
180
  'type': 'news',
181
  'news_items': news_items,
182
- 'script': script,
183
- 'duration': 2
 
184
  }
185
 
186
  def _plan_podcast_segment(self, preferences: Dict[str, Any]) -> Dict[str, Any]:
@@ -218,40 +303,85 @@ class RadioAgent:
218
  }
219
 
220
  def _generate_music_commentary(self, track: Dict[str, Any], preferences: Dict[str, Any]) -> str:
221
- """Generate commentary for music track"""
222
- if not track or not self.model:
223
  return f"Here's a great track for you!"
224
 
 
 
 
225
  try:
226
- prompt = f"""You are an energetic radio DJ. Introduce this song in 1-2 sentences:
227
  Title: {track['title']}
228
  Artist: {track['artist']}
229
  Genre: {track['genre']}
 
230
 
231
- Be enthusiastic and brief!"""
 
 
 
 
 
 
232
 
233
- response = self.model.generate_content(prompt)
234
- return response.text
 
 
 
 
 
235
  except Exception as e:
236
- return f"Coming up: {track['title']} by {track['artist']}!"
237
 
238
- def _generate_news_script(self, news_items: List[Dict[str, Any]], preferences: Dict[str, Any]) -> str:
239
- """Generate news segment script"""
240
  if not news_items:
241
- return "That's all for news. Back to music!"
242
 
243
- if self.model:
244
  try:
245
- news_text = "\n".join([f"- {item['title']}: {item['summary']}" for item in news_items[:2]])
246
- prompt = f"""You are a radio news presenter. Present these news items in a conversational,
247
- engaging way. Keep it under 100 words:
 
248
 
249
  {news_text}
250
 
251
- Be informative but friendly!"""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
252
 
253
- response = self.model.generate_content(prompt)
254
- return response.text
255
  except Exception as e:
256
  print(f"Error generating news script: {e}")
257
 
@@ -259,11 +389,11 @@ class RadioAgent:
259
  script = "In the news today: "
260
  for item in news_items[:2]:
261
  script += f"{item['title']}. "
262
- return script
263
 
264
  def _generate_podcast_intro(self, podcast: Dict[str, Any], preferences: Dict[str, Any]) -> str:
265
  """Generate podcast introduction"""
266
- if not podcast or not self.model:
267
  return "Time for an interesting podcast!"
268
 
269
  try:
@@ -275,14 +405,19 @@ class RadioAgent:
275
 
276
  Make listeners want to check it out!"""
277
 
278
- response = self.model.generate_content(prompt)
279
- return response.text
 
 
 
 
 
280
  except Exception as e:
281
  return f"Check out {podcast['title']} hosted by {podcast['host']}!"
282
 
283
  def _generate_story(self, mood: str, interests: List[str]) -> str:
284
  """Generate an interesting story or fun fact"""
285
- if not self.model:
286
  return "Here's a fun fact: Music can boost your mood and productivity!"
287
 
288
  try:
@@ -290,8 +425,13 @@ class RadioAgent:
290
  prompt = f"""Share a fascinating, {mood} story or fun fact about {interest}.
291
  Keep it engaging and under 100 words. Perfect for radio listeners!"""
292
 
293
- response = self.model.generate_content(prompt)
294
- return response.text
 
 
 
 
 
295
  except Exception as e:
296
  return "Here's something interesting: The world's oldest radio station has been broadcasting since 1920!"
297
 
 
3
  import random
4
  from typing import Dict, Any, List, Generator
5
  from datetime import datetime
6
+ from openai import OpenAI
7
+ # import google.generativeai as genai # Commented out - using Nebius/OpenAI instead
8
 
9
  from mcp_servers.music_server import MusicMCPServer
10
  from mcp_servers.news_server import NewsMCPServer
 
17
  def __init__(self, config):
18
  self.config = config
19
 
20
+ # Initialize Nebius/OpenAI LLM (GPT-OSS-120B) with error handling
21
+ self.client = None
22
+ if config.nebius_api_key:
23
+ try:
24
+ self.client = OpenAI(
25
+ api_key=config.nebius_api_key,
26
+ base_url=config.nebius_api_base
27
+ )
28
+ # Test the connection
29
+ self.client.models.list()
30
+ except Exception as e:
31
+ print(f"Warning: Could not initialize Nebius/OpenAI LLM: {e}")
32
+ print("Radio agent will work in fallback mode with limited LLM features")
33
+ self.client = None
34
+
35
+ # # Initialize Gemini LLM with error handling (COMMENTED OUT - for future use)
36
+ # self.model = None
37
+ # if config.google_api_key:
38
+ # try:
39
+ # genai.configure(api_key=config.google_api_key)
40
+ # self.model = genai.GenerativeModel('gemini-1.0-pro')
41
+ # except Exception as e:
42
+ # print(f"Warning: Could not initialize Gemini LLM: {e}")
43
+ # print("Radio agent will work in fallback mode with limited LLM features")
44
+ # self.model = None
45
 
46
  # Initialize MCP Servers
47
  self.music_server = MusicMCPServer()
48
  self.news_server = NewsMCPServer()
49
  self.podcast_server = PodcastMCPServer()
50
 
51
+ # Initialize RAG System (using Nebius API key)
52
+ self.rag_system = RadioRAGSystem(config.nebius_api_key, config.nebius_api_base, config.nebius_model)
53
 
54
  # Agent state
55
  self.is_streaming = False
56
  self.current_segment = None
57
  self.segment_history = []
58
 
59
+ def plan_radio_show(self, user_preferences: Dict[str, Any], duration_minutes: int = 30,
60
+ content_filter: Dict[str, bool] = None) -> List[Dict[str, Any]]:
61
  """
62
  Plan a personalized radio show based on user preferences
63
  This demonstrates autonomous planning behavior
 
65
  Args:
66
  user_preferences: User's preferences and mood
67
  duration_minutes: Total duration of the show
68
+ content_filter: Dictionary with content type filters (music, news, podcasts, stories)
69
 
70
  Returns:
71
  List of planned segments
72
  """
73
  segments = []
74
 
75
+ # Default filter - all enabled
76
+ if content_filter is None:
77
+ content_filter = {
78
+ "music": True,
79
+ "news": True,
80
+ "podcasts": True,
81
+ "stories": True
82
+ }
83
+
84
  # Get user preferences from RAG
85
  stored_prefs = self.rag_system.get_user_preferences()
86
  merged_prefs = {**stored_prefs, **user_preferences}
87
 
88
+ # Calculate segment distribution based on filter
89
  total_segments = max(5, duration_minutes // 5)
90
 
91
+ music_count = int(total_segments * self.config.music_ratio) if content_filter.get("music", True) else 0
92
+ news_count = int(total_segments * self.config.news_ratio) if content_filter.get("news", True) else 0
93
+ podcast_count = int(total_segments * self.config.podcast_ratio) if content_filter.get("podcasts", True) else 0
94
+ story_count = max(1, int(total_segments * self.config.story_ratio)) if content_filter.get("stories", True) else 0
95
 
96
  # Balance to total
97
+ total_planned = music_count + news_count + podcast_count + story_count
98
+ if total_planned == 0:
99
+ # If all filtered out, enable music as default
100
+ music_count = total_segments
101
+ else:
102
+ remaining = total_segments - total_planned
103
+ # Distribute remaining to enabled types
104
+ enabled_types = sum([content_filter.get("music", True), content_filter.get("news", True),
105
+ content_filter.get("podcasts", True), content_filter.get("stories", True)])
106
+ if enabled_types > 0:
107
+ per_type = remaining // enabled_types
108
+ if content_filter.get("music", True):
109
+ music_count += per_type
110
+ if content_filter.get("news", True):
111
+ news_count += per_type
112
+ if content_filter.get("podcasts", True):
113
+ podcast_count += per_type
114
+ if content_filter.get("stories", True):
115
+ story_count += per_type
116
 
117
  # Create segment plan
118
+ segment_types = []
119
+ if content_filter.get("music", True):
120
+ segment_types.extend(['music'] * music_count)
121
+ if content_filter.get("news", True):
122
+ segment_types.extend(['news'] * news_count)
123
+ if content_filter.get("podcasts", True):
124
+ segment_types.extend(['podcast'] * podcast_count)
125
+ if content_filter.get("stories", True):
126
+ segment_types.extend(['story'] * story_count)
127
 
128
  # Shuffle for variety
129
  random.shuffle(segment_types)
 
156
  return segments
157
 
158
  def _generate_intro(self, preferences: Dict[str, Any]) -> str:
159
+ """Generate personalized radio intro - longer and more engaging"""
160
  mood = preferences.get('mood', 'happy')
161
  name = preferences.get('name', 'friend')
162
  time_of_day = self._get_time_of_day()
163
+ interests = preferences.get('interests', ['technology'])
164
 
165
+ if self.client:
166
  try:
167
+ prompt = f"""You are a charismatic, entertaining radio host. Create a warm, engaging {time_of_day} greeting
168
+ for {name}. The listener is feeling {mood} and interested in {', '.join(interests[:2])}.
 
169
 
170
+ Make it:
171
+ - 4-6 sentences long (about 30-40 seconds of speech)
172
+ - Energetic and personal
173
+ - Include a fun fact or light joke related to their interests
174
+ - Make them excited to listen!
175
+ - Sound natural and conversational, like a real radio host"""
176
+
177
+ response = self.client.chat.completions.create(
178
+ model=self.config.nebius_model,
179
+ messages=[{"role": "user", "content": prompt}],
180
+ temperature=0.9,
181
+ max_tokens=400
182
+ )
183
+ return response.choices[0].message.content.strip()
184
  except Exception as e:
185
  print(f"Error generating intro: {e}")
186
 
187
+ # # Gemini fallback (COMMENTED OUT - for future use)
188
+ # if self.model:
189
+ # try:
190
+ # response = self.model.generate_content(prompt)
191
+ # return response.text
192
+ # except Exception as e:
193
+ # print(f"Error generating intro: {e}")
194
+
195
  # Fallback intro
196
+ return f"Good {time_of_day}, {name}! Welcome to your personal AI Radio station. We've got an amazing show lined up for you today! Did you know that music can actually boost your productivity? That's right! So sit back, relax, and let's get this party started!"
197
 
198
  def _generate_outro(self, preferences: Dict[str, Any]) -> str:
199
  """Generate personalized radio outro"""
200
  name = preferences.get('name', 'friend')
201
 
202
+ if self.client:
203
  try:
204
  prompt = f"""You are a charismatic radio host wrapping up a show.
205
  Create a warm, friendly goodbye message for {name}.
206
  Thank them for listening and invite them back. Keep it 2 sentences."""
207
 
208
+ response = self.client.chat.completions.create(
209
+ model=self.config.nebius_model,
210
+ messages=[{"role": "user", "content": prompt}],
211
+ temperature=0.8,
212
+ max_tokens=100
213
+ )
214
+ return response.choices[0].message.content.strip()
215
  except Exception as e:
216
  print(f"Error generating outro: {e}")
217
 
218
+ # # Gemini fallback (COMMENTED OUT - for future use)
219
+ # if self.model:
220
+ # try:
221
+ # response = self.model.generate_content(prompt)
222
+ # return response.text
223
+ # except Exception as e:
224
+ # print(f"Error generating outro: {e}")
225
+
226
  return f"That's all for now, {name}! Thanks for tuning in to AI Radio. Come back soon for more personalized content!"
227
 
228
  def _plan_music_segment(self, preferences: Dict[str, Any]) -> Dict[str, Any]:
 
250
  }
251
 
252
  def _plan_news_segment(self, preferences: Dict[str, Any]) -> Dict[str, Any]:
253
+ """Plan a news segment using News MCP Server - with batched generation"""
254
  interests = preferences.get('interests', ['world'])
255
  category = random.choice(interests)
256
 
257
  # Use MCP server to get news
258
+ news_items = self.news_server.fetch_news(category=category, limit=3)
259
 
260
+ # Generate news script in batches
261
+ script_batches = self._generate_news_script(news_items, preferences)
262
 
263
  return {
264
  'type': 'news',
265
  'news_items': news_items,
266
+ 'script': script_batches, # Now a list of batches
267
+ 'script_batches': script_batches, # For easy access
268
+ 'duration': 2 # Will be updated based on actual length
269
  }
270
 
271
  def _plan_podcast_segment(self, preferences: Dict[str, Any]) -> Dict[str, Any]:
 
303
  }
304
 
305
  def _generate_music_commentary(self, track: Dict[str, Any], preferences: Dict[str, Any]) -> str:
306
+ """Generate longer, more engaging commentary for music track with jokes/facts"""
307
+ if not track or not self.client:
308
  return f"Here's a great track for you!"
309
 
310
+ mood = preferences.get('mood', 'happy')
311
+ interests = preferences.get('interests', [])
312
+
313
  try:
314
+ prompt = f"""You are an energetic, entertaining radio DJ. Introduce this song in a fun, engaging way:
315
  Title: {track['title']}
316
  Artist: {track['artist']}
317
  Genre: {track['genre']}
318
+ Listener mood: {mood}
319
 
320
+ Make it:
321
+ - 4-6 sentences long (about 30-40 seconds of speech)
322
+ - Include a fun fact about the genre, artist, or music in general
323
+ - Add a light joke or witty comment
324
+ - Be enthusiastic and engaging
325
+ - Sound natural, like a real radio host
326
+ - Connect it to the listener's mood if possible"""
327
 
328
+ response = self.client.chat.completions.create(
329
+ model=self.config.nebius_model,
330
+ messages=[{"role": "user", "content": prompt}],
331
+ temperature=0.9,
332
+ max_tokens=300
333
+ )
334
+ return response.choices[0].message.content.strip()
335
  except Exception as e:
336
+ return f"Coming up: {track['title']} by {track['artist']}! This is a fantastic {track['genre']} track that's perfect for your {mood} mood. Let's enjoy this one!"
337
 
338
+ def _generate_news_script(self, news_items: List[Dict[str, Any]], preferences: Dict[str, Any]) -> List[str]:
339
+ """Generate news segment script in batches (1-2 minutes total, ~200-250 words)"""
340
  if not news_items:
341
+ return ["That's all for news. Back to music!"]
342
 
343
+ if self.client:
344
  try:
345
+ # Generate full news script (1-2 minutes of speech)
346
+ news_text = "\n".join([f"- {item['title']}: {item['summary']}" for item in news_items[:3]])
347
+ prompt = f"""You are a professional radio news presenter. Present these news items in a conversational,
348
+ engaging way. This should be 1-2 minutes of speech (about 200-250 words):
349
 
350
  {news_text}
351
 
352
+ Requirements:
353
+ - Provide context and background for each story
354
+ - Explain why these stories matter
355
+ - Use smooth transitions between stories
356
+ - Be informative but friendly and conversational
357
+ - Add brief analysis or implications when relevant
358
+ - Sound natural, like a real radio news anchor"""
359
+
360
+ response = self.client.chat.completions.create(
361
+ model=self.config.nebius_model,
362
+ messages=[{"role": "user", "content": prompt}],
363
+ temperature=0.7,
364
+ max_tokens=600
365
+ )
366
+ full_script = response.choices[0].message.content.strip()
367
+
368
+ # Split into batches (approximately 50-60 words each for smooth playback)
369
+ # This allows streaming without waiting for full generation
370
+ sentences = full_script.split('. ')
371
+ batches = []
372
+ current_batch = ""
373
+
374
+ for sentence in sentences:
375
+ if len(current_batch.split()) < 50:
376
+ current_batch += sentence + ". "
377
+ else:
378
+ batches.append(current_batch.strip())
379
+ current_batch = sentence + ". "
380
+
381
+ if current_batch:
382
+ batches.append(current_batch.strip())
383
 
384
+ return batches if batches else [full_script]
 
385
  except Exception as e:
386
  print(f"Error generating news script: {e}")
387
 
 
389
  script = "In the news today: "
390
  for item in news_items[:2]:
391
  script += f"{item['title']}. "
392
+ return [script]
393
 
394
  def _generate_podcast_intro(self, podcast: Dict[str, Any], preferences: Dict[str, Any]) -> str:
395
  """Generate podcast introduction"""
396
+ if not podcast or not self.client:
397
  return "Time for an interesting podcast!"
398
 
399
  try:
 
405
 
406
  Make listeners want to check it out!"""
407
 
408
+ response = self.client.chat.completions.create(
409
+ model=self.config.nebius_model,
410
+ messages=[{"role": "user", "content": prompt}],
411
+ temperature=0.8,
412
+ max_tokens=150
413
+ )
414
+ return response.choices[0].message.content.strip()
415
  except Exception as e:
416
  return f"Check out {podcast['title']} hosted by {podcast['host']}!"
417
 
418
  def _generate_story(self, mood: str, interests: List[str]) -> str:
419
  """Generate an interesting story or fun fact"""
420
+ if not self.client:
421
  return "Here's a fun fact: Music can boost your mood and productivity!"
422
 
423
  try:
 
425
  prompt = f"""Share a fascinating, {mood} story or fun fact about {interest}.
426
  Keep it engaging and under 100 words. Perfect for radio listeners!"""
427
 
428
+ response = self.client.chat.completions.create(
429
+ model=self.config.nebius_model,
430
+ messages=[{"role": "user", "content": prompt}],
431
+ temperature=0.9,
432
+ max_tokens=200
433
+ )
434
+ return response.choices[0].message.content.strip()
435
  except Exception as e:
436
  return "Here's something interesting: The world's oldest radio station has been broadcasting since 1920!"
437
 
rag_system.py CHANGED
@@ -6,20 +6,57 @@ from datetime import datetime
6
  from llama_index.core import VectorStoreIndex, Document, Settings
7
  from llama_index.core.storage.storage_context import StorageContext
8
  from llama_index.core.vector_stores import SimpleVectorStore
9
- from llama_index.embeddings.gemini import GeminiEmbedding
10
- from llama_index.llms.gemini import Gemini
 
11
 
12
  class RadioRAGSystem:
13
  """RAG system for storing and retrieving user preferences and listening history"""
14
 
15
- def __init__(self, google_api_key: str):
16
- """Initialize RAG system with LlamaIndex"""
17
- self.google_api_key = google_api_key
 
 
 
 
18
 
19
- # Configure LlamaIndex settings
20
- if google_api_key:
21
- Settings.llm = Gemini(api_key=google_api_key, model="models/gemini-pro")
22
- Settings.embed_model = GeminiEmbedding(api_key=google_api_key, model_name="models/embedding-001")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
23
 
24
  # Initialize vector store
25
  self.vector_store = SimpleVectorStore()
@@ -39,11 +76,15 @@ class RadioRAGSystem:
39
  with open(self.user_data_file, 'r') as f:
40
  data = json.load(f)
41
  self.documents = [Document(text=json.dumps(d)) for d in data]
42
- if self.documents and self.google_api_key:
43
- self.index = VectorStoreIndex.from_documents(
44
- self.documents,
45
- storage_context=self.storage_context
46
- )
 
 
 
 
47
  except Exception as e:
48
  print(f"Error loading user data: {e}")
49
 
@@ -67,12 +108,16 @@ class RadioRAGSystem:
67
  doc = Document(text=json.dumps(pref_doc))
68
  self.documents.append(doc)
69
 
70
- # Rebuild index
71
- if self.google_api_key:
72
- self.index = VectorStoreIndex.from_documents(
73
- self.documents,
74
- storage_context=self.storage_context
75
- )
 
 
 
 
76
 
77
  self._save_user_data()
78
 
@@ -89,12 +134,16 @@ class RadioRAGSystem:
89
  doc = Document(text=json.dumps(history_doc))
90
  self.documents.append(doc)
91
 
92
- # Rebuild index
93
- if self.google_api_key:
94
- self.index = VectorStoreIndex.from_documents(
95
- self.documents,
96
- storage_context=self.storage_context
97
- )
 
 
 
 
98
 
99
  self._save_user_data()
100
 
@@ -114,7 +163,7 @@ class RadioRAGSystem:
114
 
115
  def get_recommendations(self, query: str) -> Dict[str, Any]:
116
  """Get personalized recommendations based on user history and preferences"""
117
- if not self.index or not self.google_api_key:
118
  return self._get_default_recommendations()
119
 
120
  try:
 
6
  from llama_index.core import VectorStoreIndex, Document, Settings
7
  from llama_index.core.storage.storage_context import StorageContext
8
  from llama_index.core.vector_stores import SimpleVectorStore
9
+ from llama_index.llms.openai import OpenAI as LlamaOpenAI
10
+ # from llama_index.embeddings.gemini import GeminiEmbedding # Commented out - using Nebius instead
11
+ # from llama_index.llms.gemini import Gemini # Commented out - using Nebius instead
12
 
13
  class RadioRAGSystem:
14
  """RAG system for storing and retrieving user preferences and listening history"""
15
 
16
+ def __init__(self, nebius_api_key: str, nebius_api_base: str, nebius_model: str):
17
+ """Initialize RAG system with LlamaIndex using Nebius/OpenAI"""
18
+ self.nebius_api_key = nebius_api_key
19
+ self.nebius_api_base = nebius_api_base
20
+ self.nebius_model = nebius_model
21
+ self.llm_available = False
22
+ self.embedding_available = False
23
 
24
+ # Configure LlamaIndex settings with Nebius/OpenAI
25
+ if nebius_api_key:
26
+ try:
27
+ Settings.llm = LlamaOpenAI(
28
+ api_key=nebius_api_key,
29
+ api_base=nebius_api_base,
30
+ model=nebius_model,
31
+ temperature=0.7
32
+ )
33
+ self.llm_available = True
34
+ except Exception as e:
35
+ print(f"Warning: Could not initialize Nebius/OpenAI LLM: {e}")
36
+ print("RAG system will work in fallback mode without LLM features")
37
+ self.llm_available = False
38
+
39
+ # For embeddings, we'll use a simple approach or skip for now
40
+ # Embeddings can be added later if needed
41
+ self.embedding_available = False # Disabled for now - can add OpenAI embeddings later
42
+
43
+ # # Configure LlamaIndex settings with Gemini (COMMENTED OUT - for future use)
44
+ # if google_api_key:
45
+ # try:
46
+ # Settings.llm = Gemini(api_key=google_api_key, model="models/gemini-1.0-pro")
47
+ # self.llm_available = True
48
+ # except Exception as e:
49
+ # print(f"Warning: Could not initialize Gemini LLM: {e}")
50
+ # print("RAG system will work in fallback mode without LLM features")
51
+ # self.llm_available = False
52
+ #
53
+ # try:
54
+ # Settings.embed_model = GeminiEmbedding(api_key=google_api_key, model_name="models/embedding-001")
55
+ # self.embedding_available = True
56
+ # except Exception as e:
57
+ # print(f"Warning: Could not initialize Gemini Embeddings: {e}")
58
+ # print("RAG system will work in fallback mode without embeddings")
59
+ # self.embedding_available = False
60
 
61
  # Initialize vector store
62
  self.vector_store = SimpleVectorStore()
 
76
  with open(self.user_data_file, 'r') as f:
77
  data = json.load(f)
78
  self.documents = [Document(text=json.dumps(d)) for d in data]
79
+ if self.documents and self.embedding_available:
80
+ try:
81
+ self.index = VectorStoreIndex.from_documents(
82
+ self.documents,
83
+ storage_context=self.storage_context
84
+ )
85
+ except Exception as e:
86
+ print(f"Warning: Could not build vector index: {e}")
87
+ self.index = None
88
  except Exception as e:
89
  print(f"Error loading user data: {e}")
90
 
 
108
  doc = Document(text=json.dumps(pref_doc))
109
  self.documents.append(doc)
110
 
111
+ # Rebuild index if embeddings are available
112
+ if self.embedding_available:
113
+ try:
114
+ self.index = VectorStoreIndex.from_documents(
115
+ self.documents,
116
+ storage_context=self.storage_context
117
+ )
118
+ except Exception as e:
119
+ print(f"Warning: Could not rebuild index: {e}")
120
+ self.index = None
121
 
122
  self._save_user_data()
123
 
 
134
  doc = Document(text=json.dumps(history_doc))
135
  self.documents.append(doc)
136
 
137
+ # Rebuild index if embeddings are available
138
+ if self.embedding_available:
139
+ try:
140
+ self.index = VectorStoreIndex.from_documents(
141
+ self.documents,
142
+ storage_context=self.storage_context
143
+ )
144
+ except Exception as e:
145
+ print(f"Warning: Could not rebuild index: {e}")
146
+ self.index = None
147
 
148
  self._save_user_data()
149
 
 
163
 
164
  def get_recommendations(self, query: str) -> Dict[str, Any]:
165
  """Get personalized recommendations based on user history and preferences"""
166
+ if not self.index or not self.llm_available:
167
  return self._get_default_recommendations()
168
 
169
  try:
requirements.txt CHANGED
@@ -1,9 +1,11 @@
1
  gradio==4.44.0
2
- google-generativeai>=0.5.2,<0.6.0
 
3
  elevenlabs==1.10.0
4
  llama-index==0.11.20
5
- llama-index-llms-gemini==0.3.4
6
- llama-index-embeddings-gemini==0.2.2
 
7
  requests==2.32.3
8
  python-dotenv==1.0.1
9
  pydantic==2.9.2
@@ -14,3 +16,9 @@ sse-starlette==2.1.3
14
  pydantic-settings==2.5.2
15
  chromadb==0.5.5
16
  audioop-lts
 
 
 
 
 
 
 
1
  gradio==4.44.0
2
+ openai>=1.0.0
3
+ # google-generativeai>=0.5.2,<0.6.0 # Commented out - using Nebius instead
4
  elevenlabs==1.10.0
5
  llama-index==0.11.20
6
+ llama-index-llms-openai>=0.1.5
7
+ # llama-index-llms-gemini==0.3.4 # Commented out - using Nebius instead
8
+ # llama-index-embeddings-gemini==0.2.2 # Commented out - using Nebius instead
9
  requests==2.32.3
10
  python-dotenv==1.0.1
11
  pydantic==2.9.2
 
16
  pydantic-settings==2.5.2
17
  chromadb==0.5.5
18
  audioop-lts
19
+ speechrecognition>=3.10.0
20
+ # pyaudio>=0.2.11 # Optional - requires PortAudio. Install with: brew install portaudio (macOS) or apt-get install portaudio19-dev (Linux)
21
+ yt-dlp>=2024.1.0
22
+ pydub>=0.25.1
23
+ # Note: ffmpeg is required for audio conversion
24
+ # Install with: brew install ffmpeg (macOS) or sudo apt-get install ffmpeg (Linux)
tts_service.py CHANGED
@@ -43,11 +43,11 @@ class TTSService:
43
  try:
44
  voice_to_use = voice_id or self.voice_id
45
 
46
- # Generate audio
47
  audio = self.client.generate(
48
  text=text,
49
  voice=voice_to_use,
50
- model="eleven_monolingual_v1"
51
  )
52
 
53
  # Convert generator to bytes
@@ -79,11 +79,11 @@ class TTSService:
79
  try:
80
  voice_to_use = voice_id or self.voice_id
81
 
82
- # Stream audio
83
  audio_stream = self.client.generate(
84
  text=text,
85
  voice=voice_to_use,
86
- model="eleven_monolingual_v1",
87
  stream=True
88
  )
89
 
 
43
  try:
44
  voice_to_use = voice_id or self.voice_id
45
 
46
+ # Generate audio using newer model (free tier compatible)
47
  audio = self.client.generate(
48
  text=text,
49
  voice=voice_to_use,
50
+ model="eleven_turbo_v2_5" # Updated to newer model for free tier
51
  )
52
 
53
  # Convert generator to bytes
 
79
  try:
80
  voice_to_use = voice_id or self.voice_id
81
 
82
+ # Stream audio using newer model (free tier compatible)
83
  audio_stream = self.client.generate(
84
  text=text,
85
  voice=voice_to_use,
86
+ model="eleven_turbo_v2_5", # Updated to newer model for free tier
87
  stream=True
88
  )
89
 
voice_input.py ADDED
@@ -0,0 +1,161 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Voice Input Service for Speech Recognition"""
2
+ import speech_recognition as sr
3
+ from typing import Optional, Callable
4
+ import io
5
+
6
+ class VoiceInputService:
7
+ """Service for handling voice input and speech recognition"""
8
+
9
+ def __init__(self):
10
+ """Initialize voice input service"""
11
+ self.recognizer = sr.Recognizer()
12
+ self.microphone = None
13
+ self.available = False
14
+
15
+ try:
16
+ # Try to initialize microphone (requires pyaudio)
17
+ self.microphone = sr.Microphone()
18
+ # Adjust for ambient noise
19
+ with self.microphone as source:
20
+ self.recognizer.adjust_for_ambient_noise(source, duration=0.5)
21
+ self.available = True
22
+ except OSError as e:
23
+ print(f"Warning: Could not initialize microphone: {e}")
24
+ print("Voice input will not be available")
25
+ print("To enable voice input, install PortAudio:")
26
+ print(" macOS: brew install portaudio")
27
+ print(" Linux: sudo apt-get install portaudio19-dev")
28
+ print(" Then: pip install pyaudio")
29
+ self.available = False
30
+ except Exception as e:
31
+ print(f"Warning: Could not initialize microphone: {e}")
32
+ print("Voice input will not be available")
33
+ self.available = False
34
+
35
+ def listen_and_recognize(self, timeout: int = 5, phrase_time_limit: int = 10) -> Optional[str]:
36
+ """
37
+ Listen to microphone and recognize speech
38
+
39
+ Args:
40
+ timeout: Maximum time to wait for speech to start
41
+ phrase_time_limit: Maximum time for a phrase
42
+
43
+ Returns:
44
+ Recognized text or None if error
45
+ """
46
+ if not self.available or not self.microphone:
47
+ return None
48
+
49
+ try:
50
+ with self.microphone as source:
51
+ print("Listening... Speak now!")
52
+ audio = self.recognizer.listen(
53
+ source,
54
+ timeout=timeout,
55
+ phrase_time_limit=phrase_time_limit
56
+ )
57
+
58
+ print("Processing speech...")
59
+ # Use Google's free speech recognition API
60
+ text = self.recognizer.recognize_google(audio)
61
+ print(f"Recognized: {text}")
62
+ return text
63
+
64
+ except sr.WaitTimeoutError:
65
+ print("No speech detected within timeout")
66
+ return None
67
+ except sr.UnknownValueError:
68
+ print("Could not understand audio")
69
+ return None
70
+ except sr.RequestError as e:
71
+ print(f"Error with speech recognition service: {e}")
72
+ return None
73
+ except Exception as e:
74
+ print(f"Error during voice recognition: {e}")
75
+ return None
76
+
77
+ def process_song_request(self, recognized_text: str) -> dict:
78
+ """
79
+ Process a song request from recognized speech
80
+
81
+ Args:
82
+ recognized_text: Text recognized from speech
83
+
84
+ Returns:
85
+ Dictionary with song request details
86
+ """
87
+ text_lower = recognized_text.lower()
88
+
89
+ # Extract keywords
90
+ request = {
91
+ "original_text": recognized_text,
92
+ "action": None,
93
+ "song": None,
94
+ "artist": None,
95
+ "genre": None,
96
+ "mood": None
97
+ }
98
+
99
+ # Remove common action words to get the actual query
100
+ # Order matters - longer phrases first
101
+ action_phrases = [
102
+ "i want to hear", "i want to", "want to hear",
103
+ "i'd like to hear", "i would like to hear",
104
+ "play", "put on", "listen to", "i want",
105
+ "can you", "please", "i'd like", "i would like"
106
+ ]
107
+ cleaned_text = recognized_text.lower()
108
+ for phrase in action_phrases:
109
+ if phrase in cleaned_text:
110
+ cleaned_text = cleaned_text.replace(phrase, "").strip()
111
+ break # Only remove one phrase
112
+
113
+ # Clean up extra spaces and remove standalone "i", "a", "the"
114
+ words = cleaned_text.split()
115
+ words = [w for w in words if w not in ["i", "a", "an", "the"]]
116
+ cleaned_text = " ".join(words).strip()
117
+
118
+ # Detect action
119
+ if any(word in text_lower for word in ["play", "put on", "listen to", "want to hear"]):
120
+ request["action"] = "play"
121
+ elif any(word in text_lower for word in ["skip", "next", "change"]):
122
+ request["action"] = "skip"
123
+ else:
124
+ request["action"] = "play" # Default
125
+
126
+ # Try to extract song/artist/genre
127
+ # Simple keyword extraction - can be enhanced with NLP
128
+ if "by" in text_lower:
129
+ parts = text_lower.split("by")
130
+ if len(parts) == 2:
131
+ request["song"] = parts[0].strip()
132
+ request["artist"] = parts[1].strip()
133
+ else:
134
+ # If no "by", treat the cleaned text as the song/query
135
+ # But remove genre/mood words that are already extracted
136
+ song_text = cleaned_text if cleaned_text else recognized_text
137
+ if request.get("genre"):
138
+ # Remove genre from song text
139
+ song_text = song_text.replace(request["genre"], "").strip()
140
+ if request.get("mood"):
141
+ # Remove mood from song text
142
+ song_text = song_text.replace(request["mood"], "").strip()
143
+ song_text = " ".join(song_text.split()) # Clean up spaces
144
+ request["song"] = song_text if song_text else recognized_text
145
+
146
+ # Check for genre keywords
147
+ genres = ["pop", "rock", "jazz", "classical", "electronic", "hip-hop", "hip hop", "country", "indie", "rap", "blues", "folk"]
148
+ for genre in genres:
149
+ if genre in text_lower:
150
+ request["genre"] = genre
151
+ break
152
+
153
+ # Check for mood keywords
154
+ moods = ["happy", "sad", "energetic", "calm", "relaxed", "focused", "upbeat", "chill"]
155
+ for mood in moods:
156
+ if mood in text_lower:
157
+ request["mood"] = mood
158
+ break
159
+
160
+ return request
161
+