Spaces:
Sleeping
Sleeping
Nikita Makarov
commited on
Commit
·
4cdaf71
1
Parent(s):
a369c53
v1
Browse files- INSTALL_FFMPEG.md +52 -0
- INSTALL_VOICE_INPUT.md +82 -0
- app.py +314 -35
- config.py +2 -0
- mcp_servers/music_server.py +350 -18
- mcp_servers/news_server.py +78 -31
- radio_agent.py +204 -64
- rag_system.py +76 -27
- requirements.txt +11 -3
- tts_service.py +4 -4
- voice_input.py +161 -0
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
|
|
|
|
| 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 |
-
|
| 91 |
-
|
|
|
|
|
|
|
| 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 |
-
|
| 97 |
-
tts_service.save_audio(audio_bytes,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 98 |
|
| 99 |
elif segment["type"] == "music":
|
| 100 |
-
# For music
|
| 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 |
-
|
| 106 |
-
tts_service.save_audio(audio_bytes,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 114 |
-
tts_service.save_audio(audio_bytes,
|
| 115 |
|
| 116 |
# Progress info
|
| 117 |
progress = f"Segment {radio_state['current_segment_index']}/{len(radio_state['planned_show'])}"
|
| 118 |
|
| 119 |
-
|
|
|
|
| 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 |
-
|
| 141 |
|
| 142 |
**{track['title']}**
|
| 143 |
by {track['artist']}
|
| 144 |
|
| 145 |
Genre: {track['genre']}
|
| 146 |
-
Duration: {track
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 156 |
news_text += "\n\n**Headlines:**\n"
|
| 157 |
for item in news_items[:2]:
|
| 158 |
-
news_text += f"\n• {item
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
| 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=
|
| 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 |
-
|
|
|
|
|
|
|
| 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
|
| 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 |
-
#
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
|
|
|
| 41 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 42 |
demo_tracks = {
|
| 43 |
"pop": [
|
| 44 |
-
{
|
| 45 |
-
|
| 46 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 47 |
],
|
| 48 |
"rock": [
|
| 49 |
-
{
|
| 50 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 51 |
],
|
| 52 |
"jazz": [
|
| 53 |
-
{
|
| 54 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 55 |
],
|
| 56 |
"classical": [
|
| 57 |
-
{
|
| 58 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 59 |
],
|
| 60 |
"electronic": [
|
| 61 |
-
{
|
| 62 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
#
|
| 15 |
self.news_feeds = {
|
| 16 |
"technology": [
|
| 17 |
-
"https://
|
| 18 |
-
"https://
|
|
|
|
| 19 |
],
|
| 20 |
"world": [
|
| 21 |
-
"https://
|
| 22 |
-
"https://
|
|
|
|
| 23 |
],
|
| 24 |
"business": [
|
| 25 |
-
"https://feeds.
|
|
|
|
| 26 |
],
|
| 27 |
"entertainment": [
|
| 28 |
-
"https://
|
|
|
|
| 29 |
],
|
| 30 |
"science": [
|
| 31 |
-
"https://
|
|
|
|
| 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 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
print(f"Error fetching from {feed_url}: {e}")
|
| 64 |
continue
|
| 65 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 66 |
return all_news[:limit]
|
| 67 |
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 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 |
-
|
|
|
|
| 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
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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.
|
| 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
|
|
|
|
| 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 |
-
|
| 67 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 68 |
|
| 69 |
# Create segment plan
|
| 70 |
-
segment_types =
|
| 71 |
-
|
| 72 |
-
['
|
| 73 |
-
|
| 74 |
-
['
|
| 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.
|
| 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}
|
| 117 |
-
Make them excited to listen!"""
|
| 118 |
|
| 119 |
-
|
| 120 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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.
|
| 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.
|
| 138 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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=
|
| 175 |
|
| 176 |
-
# Generate news script
|
| 177 |
-
|
| 178 |
|
| 179 |
return {
|
| 180 |
'type': 'news',
|
| 181 |
'news_items': news_items,
|
| 182 |
-
'script':
|
| 183 |
-
'
|
|
|
|
| 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.
|
| 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
|
| 227 |
Title: {track['title']}
|
| 228 |
Artist: {track['artist']}
|
| 229 |
Genre: {track['genre']}
|
|
|
|
| 230 |
|
| 231 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 232 |
|
| 233 |
-
response = self.
|
| 234 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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.
|
| 244 |
try:
|
| 245 |
-
|
| 246 |
-
|
| 247 |
-
|
|
|
|
| 248 |
|
| 249 |
{news_text}
|
| 250 |
|
| 251 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 252 |
|
| 253 |
-
|
| 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.
|
| 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.
|
| 279 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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.
|
| 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.
|
| 294 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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.
|
| 10 |
-
from llama_index.
|
|
|
|
| 11 |
|
| 12 |
class RadioRAGSystem:
|
| 13 |
"""RAG system for storing and retrieving user preferences and listening history"""
|
| 14 |
|
| 15 |
-
def __init__(self,
|
| 16 |
-
"""Initialize RAG system with LlamaIndex"""
|
| 17 |
-
self.
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
|
| 19 |
-
# Configure LlamaIndex settings
|
| 20 |
-
if
|
| 21 |
-
|
| 22 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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.
|
| 43 |
-
|
| 44 |
-
self.
|
| 45 |
-
|
| 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.
|
| 72 |
-
|
| 73 |
-
self.
|
| 74 |
-
|
| 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.
|
| 94 |
-
|
| 95 |
-
self.
|
| 96 |
-
|
| 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.
|
| 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 |
-
|
|
|
|
| 3 |
elevenlabs==1.10.0
|
| 4 |
llama-index==0.11.20
|
| 5 |
-
llama-index-llms-
|
| 6 |
-
llama-index-
|
|
|
|
| 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="
|
| 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="
|
| 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 |
+
|