Spaces:
Sleeping
Sleeping
Nikita Makarov
commited on
Commit
Β·
c94876b
1
Parent(s):
5e41eee
v2.1 - works - but without syncs of voice and music.
Browse files- README.md +1 -1
- run.py +1 -1
- src/app.py +450 -181
- src/mcp_servers/music_server.py +45 -6
- src/mcp_servers/podcast_server.py +20 -0
- src/radio_agent.py +84 -25
- src/rag_system.py +12 -4
README.md
CHANGED
|
@@ -147,7 +147,7 @@ cd src
|
|
| 147 |
python app.py
|
| 148 |
```
|
| 149 |
|
| 150 |
-
5. Open your browser to `http://localhost:
|
| 151 |
|
| 152 |
## π How to Use
|
| 153 |
|
|
|
|
| 147 |
python app.py
|
| 148 |
```
|
| 149 |
|
| 150 |
+
5. Open your browser to `http://localhost:7870`
|
| 151 |
|
| 152 |
## π How to Use
|
| 153 |
|
run.py
CHANGED
|
@@ -12,7 +12,7 @@ from app import demo
|
|
| 12 |
if __name__ == "__main__":
|
| 13 |
demo.launch(
|
| 14 |
server_name="0.0.0.0",
|
| 15 |
-
server_port=
|
| 16 |
share=False
|
| 17 |
)
|
| 18 |
|
|
|
|
| 12 |
if __name__ == "__main__":
|
| 13 |
demo.launch(
|
| 14 |
server_name="0.0.0.0",
|
| 15 |
+
server_port=7871,
|
| 16 |
share=False
|
| 17 |
)
|
| 18 |
|
src/app.py
CHANGED
|
@@ -22,9 +22,61 @@ from rag_system import RadioRAGSystem
|
|
| 22 |
from voice_input import VoiceInputService
|
| 23 |
from user_memory import UserMemoryService
|
| 24 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 25 |
# Global state
|
| 26 |
radio_state = {
|
| 27 |
"is_playing": False,
|
|
|
|
| 28 |
"current_segment_index": 0,
|
| 29 |
"planned_show": [],
|
| 30 |
"user_preferences": {},
|
|
@@ -35,12 +87,15 @@ radio_state = {
|
|
| 35 |
"last_segment": None,
|
| 36 |
"current_track": None, # Currently playing track for like/dislike
|
| 37 |
"user_id": None, # Current user ID
|
|
|
|
| 38 |
"content_filter": {
|
| 39 |
"music": True,
|
| 40 |
"news": True,
|
| 41 |
"podcasts": True,
|
| 42 |
"stories": True
|
| 43 |
-
}
|
|
|
|
|
|
|
| 44 |
}
|
| 45 |
|
| 46 |
# Initialize services
|
|
@@ -178,12 +233,9 @@ def start_radio_stream():
|
|
| 178 |
def start_and_play_first_segment():
|
| 179 |
"""
|
| 180 |
One-shot helper for the UI:
|
| 181 |
-
-
|
| 182 |
-
-
|
| 183 |
Returns everything needed to update the UI in a single call.
|
| 184 |
-
|
| 185 |
-
NOTE: Total time is ~10-15s on first click (YouTube search + LLM + TTS).
|
| 186 |
-
We print progress to console so you can see it's not stuck.
|
| 187 |
"""
|
| 188 |
print("βΆοΈ [start_and_play_first_segment] Starting...")
|
| 189 |
|
|
@@ -220,6 +272,26 @@ def start_and_play_first_segment():
|
|
| 220 |
}
|
| 221 |
print("π Using default preferences")
|
| 222 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 223 |
if radio_state["is_playing"]:
|
| 224 |
return (
|
| 225 |
"π» Radio is already playing!",
|
|
@@ -231,6 +303,9 @@ def start_and_play_first_segment():
|
|
| 231 |
import time
|
| 232 |
t0 = time.time()
|
| 233 |
|
|
|
|
|
|
|
|
|
|
| 234 |
# Step 1: Plan skeleton show (YouTube searches happen here)
|
| 235 |
print(" [1/3] Planning show (searching YouTube)...")
|
| 236 |
user_id = radio_state.get("user_id")
|
|
@@ -246,10 +321,7 @@ def start_and_play_first_segment():
|
|
| 246 |
radio_state["current_segment_index"] = 0
|
| 247 |
radio_state["is_playing"] = True
|
| 248 |
radio_state["stop_flag"] = False
|
| 249 |
-
radio_state["
|
| 250 |
-
radio_state["news_total_batches"] = 0
|
| 251 |
-
radio_state["news_batches_played"] = 0
|
| 252 |
-
radio_state["last_segment"] = None
|
| 253 |
|
| 254 |
# Step 2 & 3: Generate and play first segment (LLM + TTS inside play_next_segment)
|
| 255 |
print(" [2/3] Generating first segment (LLM)...")
|
|
@@ -271,8 +343,20 @@ def start_and_play_first_segment():
|
|
| 271 |
|
| 272 |
def play_next_segment():
|
| 273 |
"""Play the next segment in the show - returns host audio and music audio separately"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 274 |
if not radio_state["is_playing"]:
|
| 275 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 276 |
|
| 277 |
if radio_state["stop_flag"]:
|
| 278 |
radio_state["is_playing"] = False
|
|
@@ -416,47 +500,53 @@ def play_next_segment():
|
|
| 416 |
youtube_id = track["url"].split("v=")[-1].split("&")[0]
|
| 417 |
|
| 418 |
if youtube_id:
|
| 419 |
-
#
|
| 420 |
-
|
| 421 |
-
|
| 422 |
-
|
| 423 |
-
|
| 424 |
-
|
| 425 |
-
|
| 426 |
-
|
| 427 |
-
|
| 428 |
-
|
|
|
|
|
|
|
|
|
|
| 429 |
|
| 430 |
-
#
|
| 431 |
-
|
| 432 |
-
|
| 433 |
-
|
| 434 |
-
|
| 435 |
-
|
| 436 |
-
|
| 437 |
-
|
| 438 |
-
|
| 439 |
-
|
| 440 |
-
|
| 441 |
-
|
| 442 |
-
|
| 443 |
-
|
| 444 |
-
|
| 445 |
-
|
| 446 |
-
|
| 447 |
-
|
| 448 |
-
|
| 449 |
-
|
| 450 |
-
|
| 451 |
-
|
| 452 |
-
|
| 453 |
-
|
| 454 |
-
|
| 455 |
-
|
| 456 |
-
|
| 457 |
-
|
| 458 |
-
|
| 459 |
-
|
|
|
|
|
|
|
|
|
|
| 460 |
else:
|
| 461 |
# Fallback: Just show link
|
| 462 |
music_player_html = f"""
|
|
@@ -526,47 +616,52 @@ def play_next_segment():
|
|
| 526 |
if podcast.get("source") == "youtube" and podcast.get("youtube_id"):
|
| 527 |
youtube_id = podcast.get("youtube_id", "")
|
| 528 |
|
| 529 |
-
#
|
| 530 |
-
|
| 531 |
-
|
| 532 |
-
|
| 533 |
-
|
| 534 |
-
|
| 535 |
-
|
| 536 |
-
|
| 537 |
-
|
| 538 |
-
|
| 539 |
-
|
| 540 |
-
|
| 541 |
-
|
| 542 |
-
|
| 543 |
-
|
| 544 |
-
|
| 545 |
-
|
| 546 |
-
|
| 547 |
-
|
| 548 |
-
|
| 549 |
-
|
| 550 |
-
|
| 551 |
-
|
| 552 |
-
|
| 553 |
-
|
| 554 |
-
|
| 555 |
-
|
| 556 |
-
|
| 557 |
-
|
| 558 |
-
|
| 559 |
-
|
| 560 |
-
|
| 561 |
-
|
| 562 |
-
|
| 563 |
-
|
| 564 |
-
|
| 565 |
-
|
| 566 |
-
|
| 567 |
-
|
| 568 |
-
|
| 569 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 570 |
elif podcast.get("url") and "youtube" in podcast.get("url", ""):
|
| 571 |
# Fallback: extract YouTube ID from URL and create simple embed
|
| 572 |
url = podcast.get("url", "")
|
|
@@ -577,45 +672,50 @@ def play_next_segment():
|
|
| 577 |
youtube_id = url.split("youtu.be/")[-1].split("?")[0]
|
| 578 |
|
| 579 |
if youtube_id:
|
| 580 |
-
#
|
| 581 |
-
|
| 582 |
-
|
| 583 |
-
|
| 584 |
-
|
| 585 |
-
|
| 586 |
-
|
| 587 |
-
|
| 588 |
-
|
|
|
|
| 589 |
|
| 590 |
-
|
| 591 |
-
|
| 592 |
-
|
| 593 |
-
|
| 594 |
-
|
| 595 |
-
|
| 596 |
-
|
| 597 |
-
|
| 598 |
-
|
| 599 |
-
|
| 600 |
-
|
| 601 |
-
|
| 602 |
-
|
| 603 |
-
|
| 604 |
-
|
| 605 |
-
|
| 606 |
-
|
| 607 |
-
|
| 608 |
-
|
| 609 |
-
|
| 610 |
-
|
| 611 |
-
|
| 612 |
-
|
| 613 |
-
|
| 614 |
-
|
| 615 |
-
|
| 616 |
-
|
| 617 |
-
|
| 618 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 619 |
|
| 620 |
# Progress info
|
| 621 |
progress = f"Segment {radio_state['current_segment_index']}/{len(radio_state['planned_show'])}"
|
|
@@ -654,21 +754,38 @@ def play_next_segment():
|
|
| 654 |
return segment_info, host_audio_file, music_player_html, progress, get_now_playing(segment), display_script
|
| 655 |
|
| 656 |
def stop_radio():
|
| 657 |
-
"""Stop the radio stream -
|
| 658 |
radio_state["stop_flag"] = True
|
| 659 |
radio_state["is_playing"] = False
|
| 660 |
-
radio_state["
|
| 661 |
-
|
|
|
|
|
|
|
|
|
|
| 662 |
|
| 663 |
-
|
|
|
|
|
|
|
| 664 |
return (
|
| 665 |
-
|
| 666 |
None, # Clear audio
|
| 667 |
"", # Clear music player HTML
|
| 668 |
-
"Stopped",
|
| 669 |
-
"
|
| 670 |
)
|
| 671 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 672 |
def format_segment_info(segment: Dict[str, Any]) -> str:
|
| 673 |
"""Format segment information for display"""
|
| 674 |
seg_type = segment["type"]
|
|
@@ -849,48 +966,53 @@ def handle_voice_request():
|
|
| 849 |
youtube_id = track["url"].split("v=")[-1].split("&")[0]
|
| 850 |
|
| 851 |
if youtube_id:
|
| 852 |
-
#
|
| 853 |
-
|
| 854 |
-
|
| 855 |
-
|
| 856 |
-
|
| 857 |
-
|
| 858 |
-
|
| 859 |
-
|
| 860 |
-
|
| 861 |
-
|
|
|
|
| 862 |
|
| 863 |
-
#
|
| 864 |
-
|
| 865 |
-
|
| 866 |
-
|
| 867 |
-
|
| 868 |
-
|
| 869 |
-
|
| 870 |
-
|
| 871 |
-
|
| 872 |
-
|
| 873 |
-
|
| 874 |
-
|
| 875 |
-
|
| 876 |
-
|
| 877 |
-
|
| 878 |
-
|
| 879 |
-
|
| 880 |
-
|
| 881 |
-
|
| 882 |
-
|
| 883 |
-
|
| 884 |
-
|
| 885 |
-
|
| 886 |
-
|
| 887 |
-
|
| 888 |
-
|
| 889 |
-
|
| 890 |
-
|
| 891 |
-
|
| 892 |
-
|
| 893 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 894 |
else:
|
| 895 |
music_player_html = f"""
|
| 896 |
<div style="padding: 1rem; background: #f0f0f0; border-radius: 10px; margin: 1rem 0;">
|
|
@@ -1156,8 +1278,133 @@ custom_css = """
|
|
| 1156 |
}
|
| 1157 |
"""
|
| 1158 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1159 |
# Build Gradio Interface
|
| 1160 |
-
with gr.Blocks(css=custom_css, title="AI Radio π΅", theme=gr.themes.Soft()) as demo:
|
| 1161 |
|
| 1162 |
# Hidden state for user ID (persisted via localStorage)
|
| 1163 |
user_id_state = gr.State(value=None)
|
|
@@ -1314,6 +1561,27 @@ with gr.Blocks(css=custom_css, title="AI Radio π΅", theme=gr.themes.Soft()) as
|
|
| 1314 |
audio_output = gr.Audio(label="π Host Speech", autoplay=True, type="filepath", elem_id="host_audio")
|
| 1315 |
music_player = gr.HTML(label="π΅ Music/Podcast Player (streaming)")
|
| 1316 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1317 |
status_text = gr.Textbox(label="Status", value="Ready", interactive=False, visible=False)
|
| 1318 |
|
| 1319 |
# Connect buttons
|
|
@@ -1332,7 +1600,8 @@ with gr.Blocks(css=custom_css, title="AI Radio π΅", theme=gr.themes.Soft()) as
|
|
| 1332 |
stop_btn.click(
|
| 1333 |
fn=stop_radio,
|
| 1334 |
inputs=[],
|
| 1335 |
-
outputs=[status_text, audio_output, music_player, progress_text, now_playing]
|
|
|
|
| 1336 |
)
|
| 1337 |
|
| 1338 |
# Voice input button - direct click without .then() chain
|
|
@@ -1609,7 +1878,7 @@ Your passphrase: **{passphrase}**
|
|
| 1609 |
if __name__ == "__main__":
|
| 1610 |
demo.launch(
|
| 1611 |
server_name="0.0.0.0",
|
| 1612 |
-
server_port=
|
| 1613 |
share=False
|
| 1614 |
)
|
| 1615 |
|
|
|
|
| 22 |
from voice_input import VoiceInputService
|
| 23 |
from user_memory import UserMemoryService
|
| 24 |
|
| 25 |
+
|
| 26 |
+
def create_youtube_player_html(youtube_id: str, title: str, artist: str, url: str) -> str:
|
| 27 |
+
"""Create YouTube player iframe HTML"""
|
| 28 |
+
return f"""
|
| 29 |
+
<iframe width="100%" height="500"
|
| 30 |
+
src="https://www.youtube.com/embed/{youtube_id}?autoplay=1"
|
| 31 |
+
allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture"
|
| 32 |
+
allowfullscreen>
|
| 33 |
+
</iframe>
|
| 34 |
+
"""
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
def create_podcast_player_html(youtube_id: str, title: str, host: str, url: str) -> str:
|
| 38 |
+
"""Create podcast YouTube player iframe HTML"""
|
| 39 |
+
return f"""
|
| 40 |
+
<iframe width="100%" height="500"
|
| 41 |
+
src="https://www.youtube.com/embed/{youtube_id}?autoplay=1"
|
| 42 |
+
allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture"
|
| 43 |
+
allowfullscreen>
|
| 44 |
+
</iframe>
|
| 45 |
+
"""
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
def create_placeholder_html(title: str, artist: str, delay_s: float, media_type: str = "music") -> str:
|
| 49 |
+
"""Create placeholder HTML shown while waiting for host speech to end"""
|
| 50 |
+
bg_color = "#1a1a2e" if media_type == "music" else "#6b46c1"
|
| 51 |
+
icon = "π΅" if media_type == "music" else "ποΈ"
|
| 52 |
+
return f"""
|
| 53 |
+
<div style="padding: 1rem; background: linear-gradient(135deg, {bg_color} 0%, #16213e 100%); border-radius: 12px; margin: 1rem 0; box-shadow: 0 4px 15px rgba(0,0,0,0.3);">
|
| 54 |
+
<h4 style="margin: 0 0 0.75rem 0; color: #fff; font-size: 1.1em;">{icon} {title}</h4>
|
| 55 |
+
<p style="margin: 0 0 0.75rem 0; color: #aaa; font-size: 0.9em;">by {artist}</p>
|
| 56 |
+
<div style="position: relative; width: 100%; padding-bottom: 56.25%; border-radius: 8px; overflow: hidden; background: #000; display: flex; align-items: center; justify-content: center;">
|
| 57 |
+
<div style="position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); text-align: center; color: #fff;">
|
| 58 |
+
<div style="font-size: 3em; margin-bottom: 0.5rem;">β³</div>
|
| 59 |
+
<p style="margin: 0; font-size: 0.9em;">Video starts in {delay_s:.0f}s...</p>
|
| 60 |
+
<p style="margin: 0.5rem 0 0 0; font-size: 0.8em; color: #888;">After host finishes speaking</p>
|
| 61 |
+
</div>
|
| 62 |
+
</div>
|
| 63 |
+
</div>
|
| 64 |
+
"""
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
def get_pending_player():
|
| 68 |
+
"""Check if there's a pending player ready to show"""
|
| 69 |
+
pending = radio_state.get("pending_player")
|
| 70 |
+
if pending and time.time() >= pending.get("ready_at", 0):
|
| 71 |
+
html = pending.get("html", "")
|
| 72 |
+
radio_state["pending_player"] = None # Clear it
|
| 73 |
+
return html
|
| 74 |
+
return None
|
| 75 |
+
|
| 76 |
# Global state
|
| 77 |
radio_state = {
|
| 78 |
"is_playing": False,
|
| 79 |
+
"is_stopped": False, # User explicitly stopped (vs just paused)
|
| 80 |
"current_segment_index": 0,
|
| 81 |
"planned_show": [],
|
| 82 |
"user_preferences": {},
|
|
|
|
| 87 |
"last_segment": None,
|
| 88 |
"current_track": None, # Currently playing track for like/dislike
|
| 89 |
"user_id": None, # Current user ID
|
| 90 |
+
"auto_play": True, # Auto-play next segment when current finishes
|
| 91 |
"content_filter": {
|
| 92 |
"music": True,
|
| 93 |
"news": True,
|
| 94 |
"podcasts": True,
|
| 95 |
"stories": True
|
| 96 |
+
},
|
| 97 |
+
# Pending media player (to be added after host speech ends)
|
| 98 |
+
"pending_player": None, # {"html": "...", "ready_at": timestamp}
|
| 99 |
}
|
| 100 |
|
| 101 |
# Initialize services
|
|
|
|
| 233 |
def start_and_play_first_segment():
|
| 234 |
"""
|
| 235 |
One-shot helper for the UI:
|
| 236 |
+
- If resuming from stop: continue from next segment
|
| 237 |
+
- If fresh start: Plan the show and play first segment
|
| 238 |
Returns everything needed to update the UI in a single call.
|
|
|
|
|
|
|
|
|
|
| 239 |
"""
|
| 240 |
print("βΆοΈ [start_and_play_first_segment] Starting...")
|
| 241 |
|
|
|
|
| 272 |
}
|
| 273 |
print("π Using default preferences")
|
| 274 |
|
| 275 |
+
# Check if resuming from stopped state
|
| 276 |
+
if radio_state.get("is_stopped") and radio_state.get("planned_show"):
|
| 277 |
+
print("βΆοΈ Resuming from stopped state...")
|
| 278 |
+
radio_state["is_playing"] = True
|
| 279 |
+
radio_state["is_stopped"] = False
|
| 280 |
+
radio_state["stop_flag"] = False
|
| 281 |
+
|
| 282 |
+
# Play next segment (continuing from where we stopped)
|
| 283 |
+
segment_info, host_audio, music_html, progress, now_playing, llm_script = play_next_segment()
|
| 284 |
+
|
| 285 |
+
return (
|
| 286 |
+
"βΆοΈ Resuming radio...",
|
| 287 |
+
segment_info,
|
| 288 |
+
host_audio,
|
| 289 |
+
music_html,
|
| 290 |
+
progress,
|
| 291 |
+
now_playing,
|
| 292 |
+
llm_script,
|
| 293 |
+
)
|
| 294 |
+
|
| 295 |
if radio_state["is_playing"]:
|
| 296 |
return (
|
| 297 |
"π» Radio is already playing!",
|
|
|
|
| 303 |
import time
|
| 304 |
t0 = time.time()
|
| 305 |
|
| 306 |
+
# Fresh start - reset and plan new show
|
| 307 |
+
reset_radio()
|
| 308 |
+
|
| 309 |
# Step 1: Plan skeleton show (YouTube searches happen here)
|
| 310 |
print(" [1/3] Planning show (searching YouTube)...")
|
| 311 |
user_id = radio_state.get("user_id")
|
|
|
|
| 321 |
radio_state["current_segment_index"] = 0
|
| 322 |
radio_state["is_playing"] = True
|
| 323 |
radio_state["stop_flag"] = False
|
| 324 |
+
radio_state["is_stopped"] = False
|
|
|
|
|
|
|
|
|
|
| 325 |
|
| 326 |
# Step 2 & 3: Generate and play first segment (LLM + TTS inside play_next_segment)
|
| 327 |
print(" [2/3] Generating first segment (LLM)...")
|
|
|
|
| 343 |
|
| 344 |
def play_next_segment():
|
| 345 |
"""Play the next segment in the show - returns host audio and music audio separately"""
|
| 346 |
+
# If stopped, resume first
|
| 347 |
+
if radio_state.get("is_stopped"):
|
| 348 |
+
print("βΆοΈ Resuming from stopped state in play_next_segment...")
|
| 349 |
+
radio_state["is_stopped"] = False
|
| 350 |
+
radio_state["stop_flag"] = False
|
| 351 |
+
radio_state["is_playing"] = True
|
| 352 |
+
|
| 353 |
if not radio_state["is_playing"]:
|
| 354 |
+
# If we have a planned show, try to resume
|
| 355 |
+
if radio_state.get("planned_show"):
|
| 356 |
+
radio_state["is_playing"] = True
|
| 357 |
+
radio_state["stop_flag"] = False
|
| 358 |
+
else:
|
| 359 |
+
return "βΈοΈ No show planned. Click 'Generate & Play' to start.", None, None, None, "", ""
|
| 360 |
|
| 361 |
if radio_state["stop_flag"]:
|
| 362 |
radio_state["is_playing"] = False
|
|
|
|
| 500 |
youtube_id = track["url"].split("v=")[-1].split("&")[0]
|
| 501 |
|
| 502 |
if youtube_id:
|
| 503 |
+
# # COMMENTED OUT: Delayed loading mechanism
|
| 504 |
+
# # Get host audio duration to calculate delay
|
| 505 |
+
# delay_ms = 0
|
| 506 |
+
# delay_s = 0.0
|
| 507 |
+
# if host_audio_file and os.path.exists(host_audio_file):
|
| 508 |
+
# try:
|
| 509 |
+
# audio = AudioSegment.from_file(host_audio_file)
|
| 510 |
+
# duration_ms = len(audio)
|
| 511 |
+
# delay_ms = max(0, duration_ms - 3000) # Start 3 seconds before end
|
| 512 |
+
# delay_s = delay_ms / 1000.0
|
| 513 |
+
# print(f"β±οΈ Host audio: {duration_ms}ms, YouTube delay: {delay_ms}ms")
|
| 514 |
+
# except Exception as e:
|
| 515 |
+
# print(f"β οΈ Could not get audio duration: {e}")
|
| 516 |
|
| 517 |
+
# # Store the actual iframe HTML for later (after host speech ends)
|
| 518 |
+
# actual_player_html = create_youtube_player_html(
|
| 519 |
+
# youtube_id,
|
| 520 |
+
# track.get('title', 'Unknown'),
|
| 521 |
+
# track.get('artist', 'Unknown'),
|
| 522 |
+
# track['url']
|
| 523 |
+
# )
|
| 524 |
+
|
| 525 |
+
# # Schedule the player to be ready after the delay
|
| 526 |
+
# radio_state["pending_player"] = {
|
| 527 |
+
# "html": actual_player_html,
|
| 528 |
+
# "ready_at": time.time() + delay_s,
|
| 529 |
+
# "type": "music",
|
| 530 |
+
# "title": track.get('title', 'Unknown')
|
| 531 |
+
# }
|
| 532 |
+
|
| 533 |
+
# # Return placeholder initially (no iframe yet!)
|
| 534 |
+
# music_player_html = create_placeholder_html(
|
| 535 |
+
# track.get('title', 'Unknown'),
|
| 536 |
+
# track.get('artist', 'Unknown'),
|
| 537 |
+
# delay_s,
|
| 538 |
+
# "music"
|
| 539 |
+
# )
|
| 540 |
+
# print(f"β
YouTube player scheduled: {track.get('title', 'Unknown')} (ID: {youtube_id}) - iframe added after {delay_s:.1f}s")
|
| 541 |
+
|
| 542 |
+
# Simple immediate iframe - start track with host
|
| 543 |
+
music_player_html = create_youtube_player_html(
|
| 544 |
+
youtube_id,
|
| 545 |
+
track.get('title', 'Unknown'),
|
| 546 |
+
track.get('artist', 'Unknown'),
|
| 547 |
+
track['url']
|
| 548 |
+
)
|
| 549 |
+
print(f"β
YouTube player created immediately: {track.get('title', 'Unknown')} (ID: {youtube_id})")
|
| 550 |
else:
|
| 551 |
# Fallback: Just show link
|
| 552 |
music_player_html = f"""
|
|
|
|
| 616 |
if podcast.get("source") == "youtube" and podcast.get("youtube_id"):
|
| 617 |
youtube_id = podcast.get("youtube_id", "")
|
| 618 |
|
| 619 |
+
# # COMMENTED OUT: Delayed loading mechanism
|
| 620 |
+
# # Get host audio duration to calculate delay
|
| 621 |
+
# delay_ms = 0
|
| 622 |
+
# if host_audio_file and os.path.exists(host_audio_file):
|
| 623 |
+
# try:
|
| 624 |
+
# audio = AudioSegment.from_file(host_audio_file)
|
| 625 |
+
# duration_ms = len(audio)
|
| 626 |
+
# delay_ms = max(0, duration_ms - 3000) # Start 3 seconds before end
|
| 627 |
+
# print(f"β±οΈ Podcast host audio: {duration_ms}ms, YouTube delay: {delay_ms}ms")
|
| 628 |
+
# except Exception as e:
|
| 629 |
+
# print(f"β οΈ Could not get audio duration: {e}")
|
| 630 |
+
|
| 631 |
+
# # Store the actual iframe HTML for later (after host speech ends)
|
| 632 |
+
# delay_s = delay_ms / 1000.0
|
| 633 |
+
# actual_player_html = create_podcast_player_html(
|
| 634 |
+
# youtube_id,
|
| 635 |
+
# podcast.get('title', 'Podcast'),
|
| 636 |
+
# podcast.get('host', 'Unknown Host'),
|
| 637 |
+
# podcast.get('url', '#')
|
| 638 |
+
# )
|
| 639 |
+
|
| 640 |
+
# # Schedule the player to be ready after the delay
|
| 641 |
+
# radio_state["pending_player"] = {
|
| 642 |
+
# "html": actual_player_html,
|
| 643 |
+
# "ready_at": time.time() + delay_s,
|
| 644 |
+
# "type": "podcast",
|
| 645 |
+
# "title": podcast.get('title', 'Unknown')
|
| 646 |
+
# }
|
| 647 |
+
|
| 648 |
+
# # Return placeholder initially (no iframe yet!)
|
| 649 |
+
# music_player_html = create_placeholder_html(
|
| 650 |
+
# podcast.get('title', 'Podcast'),
|
| 651 |
+
# podcast.get('host', 'Unknown Host'),
|
| 652 |
+
# delay_s,
|
| 653 |
+
# "podcast"
|
| 654 |
+
# )
|
| 655 |
+
# print(f"β
Podcast YouTube player scheduled: {podcast.get('title', 'Unknown')} (ID: {youtube_id}) - iframe added after {delay_s:.1f}s")
|
| 656 |
+
|
| 657 |
+
# Simple immediate iframe - start podcast with host
|
| 658 |
+
music_player_html = create_podcast_player_html(
|
| 659 |
+
youtube_id,
|
| 660 |
+
podcast.get('title', 'Podcast'),
|
| 661 |
+
podcast.get('host', 'Unknown Host'),
|
| 662 |
+
podcast.get('url', '#')
|
| 663 |
+
)
|
| 664 |
+
print(f"β
Podcast YouTube player created immediately: {podcast.get('title', 'Unknown')} (ID: {youtube_id})")
|
| 665 |
elif podcast.get("url") and "youtube" in podcast.get("url", ""):
|
| 666 |
# Fallback: extract YouTube ID from URL and create simple embed
|
| 667 |
url = podcast.get("url", "")
|
|
|
|
| 672 |
youtube_id = url.split("youtu.be/")[-1].split("?")[0]
|
| 673 |
|
| 674 |
if youtube_id:
|
| 675 |
+
# # COMMENTED OUT: Delayed loading mechanism
|
| 676 |
+
# # Get host audio duration to calculate delay
|
| 677 |
+
# delay_ms = 0
|
| 678 |
+
# if host_audio_file and os.path.exists(host_audio_file):
|
| 679 |
+
# try:
|
| 680 |
+
# audio = AudioSegment.from_file(host_audio_file)
|
| 681 |
+
# duration_ms = len(audio)
|
| 682 |
+
# delay_ms = max(0, duration_ms - 3000)
|
| 683 |
+
# except:
|
| 684 |
+
# pass
|
| 685 |
|
| 686 |
+
# delay_s = delay_ms / 1000.0
|
| 687 |
+
# actual_player_html = create_podcast_player_html(
|
| 688 |
+
# youtube_id,
|
| 689 |
+
# podcast.get('title', 'Podcast'),
|
| 690 |
+
# podcast.get('host', 'Unknown Host'),
|
| 691 |
+
# url
|
| 692 |
+
# )
|
| 693 |
+
|
| 694 |
+
# # Schedule the player to be ready after the delay
|
| 695 |
+
# radio_state["pending_player"] = {
|
| 696 |
+
# "html": actual_player_html,
|
| 697 |
+
# "ready_at": time.time() + delay_s,
|
| 698 |
+
# "type": "podcast",
|
| 699 |
+
# "title": podcast.get('title', 'Unknown')
|
| 700 |
+
# }
|
| 701 |
+
|
| 702 |
+
# # Return placeholder initially
|
| 703 |
+
# music_player_html = create_placeholder_html(
|
| 704 |
+
# podcast.get('title', 'Podcast'),
|
| 705 |
+
# podcast.get('host', 'Unknown Host'),
|
| 706 |
+
# delay_s,
|
| 707 |
+
# "podcast"
|
| 708 |
+
# )
|
| 709 |
+
# print(f"β
Podcast iframe player scheduled: {podcast.get('title', 'Unknown')} (ID: {youtube_id}) - iframe added after {delay_s:.1f}s")
|
| 710 |
+
|
| 711 |
+
# Simple immediate iframe
|
| 712 |
+
music_player_html = create_podcast_player_html(
|
| 713 |
+
youtube_id,
|
| 714 |
+
podcast.get('title', 'Podcast'),
|
| 715 |
+
podcast.get('host', 'Unknown Host'),
|
| 716 |
+
url
|
| 717 |
+
)
|
| 718 |
+
print(f"β
Podcast iframe player created immediately: {podcast.get('title', 'Unknown')} (ID: {youtube_id})")
|
| 719 |
|
| 720 |
# Progress info
|
| 721 |
progress = f"Segment {radio_state['current_segment_index']}/{len(radio_state['planned_show'])}"
|
|
|
|
| 754 |
return segment_info, host_audio_file, music_player_html, progress, get_now_playing(segment), display_script
|
| 755 |
|
| 756 |
def stop_radio():
|
| 757 |
+
"""Stop the radio stream - pauses at current segment without resetting"""
|
| 758 |
radio_state["stop_flag"] = True
|
| 759 |
radio_state["is_playing"] = False
|
| 760 |
+
radio_state["is_stopped"] = True
|
| 761 |
+
# DON'T reset planned_show or current_segment_index - allow resuming
|
| 762 |
+
|
| 763 |
+
current_idx = radio_state.get("current_segment_index", 0)
|
| 764 |
+
total_segments = len(radio_state.get("planned_show", []))
|
| 765 |
|
| 766 |
+
status_msg = f"βΉοΈ Stopped at segment {current_idx}/{total_segments}. Click 'Generate & Play' or 'Next Segment' to continue."
|
| 767 |
+
|
| 768 |
+
# Return status, clear audio, clear music player, progress, now playing
|
| 769 |
return (
|
| 770 |
+
status_msg,
|
| 771 |
None, # Clear audio
|
| 772 |
"", # Clear music player HTML
|
| 773 |
+
f"Stopped at {current_idx}/{total_segments}",
|
| 774 |
+
f"βΈοΈ Paused - Segment {current_idx}/{total_segments}"
|
| 775 |
)
|
| 776 |
|
| 777 |
+
|
| 778 |
+
def reset_radio():
|
| 779 |
+
"""Fully reset the radio - called when starting fresh"""
|
| 780 |
+
radio_state["stop_flag"] = False
|
| 781 |
+
radio_state["is_playing"] = False
|
| 782 |
+
radio_state["is_stopped"] = False
|
| 783 |
+
radio_state["planned_show"] = []
|
| 784 |
+
radio_state["current_segment_index"] = 0
|
| 785 |
+
radio_state["current_news_batches"] = []
|
| 786 |
+
radio_state["news_total_batches"] = 0
|
| 787 |
+
radio_state["news_batches_played"] = 0
|
| 788 |
+
|
| 789 |
def format_segment_info(segment: Dict[str, Any]) -> str:
|
| 790 |
"""Format segment information for display"""
|
| 791 |
seg_type = segment["type"]
|
|
|
|
| 966 |
youtube_id = track["url"].split("v=")[-1].split("&")[0]
|
| 967 |
|
| 968 |
if youtube_id:
|
| 969 |
+
# # COMMENTED OUT: Delayed loading mechanism
|
| 970 |
+
# # Get host audio duration to calculate delay
|
| 971 |
+
# delay_ms = 0
|
| 972 |
+
# if audio_file and os.path.exists(audio_file):
|
| 973 |
+
# try:
|
| 974 |
+
# audio = AudioSegment.from_file(audio_file)
|
| 975 |
+
# duration_ms = len(audio)
|
| 976 |
+
# delay_ms = max(0, duration_ms - 3000) # Start 3 seconds before end
|
| 977 |
+
# print(f"β±οΈ Voice request audio: {duration_ms}ms, YouTube delay: {delay_ms}ms")
|
| 978 |
+
# except Exception as e:
|
| 979 |
+
# print(f"β οΈ Could not get audio duration: {e}")
|
| 980 |
|
| 981 |
+
# # Store the actual iframe HTML for later (after host speech ends)
|
| 982 |
+
# delay_s = delay_ms / 1000.0
|
| 983 |
+
# actual_player_html = create_youtube_player_html(
|
| 984 |
+
# youtube_id,
|
| 985 |
+
# track.get('title', 'Unknown'),
|
| 986 |
+
# track.get('artist', 'Unknown'),
|
| 987 |
+
# track['url']
|
| 988 |
+
# )
|
| 989 |
+
|
| 990 |
+
# # Schedule the player to be ready after the delay
|
| 991 |
+
# radio_state["pending_player"] = {
|
| 992 |
+
# "html": actual_player_html,
|
| 993 |
+
# "ready_at": time.time() + delay_s,
|
| 994 |
+
# "type": "music",
|
| 995 |
+
# "title": track.get('title', 'Unknown')
|
| 996 |
+
# }
|
| 997 |
+
|
| 998 |
+
# # Return placeholder initially (no iframe yet!)
|
| 999 |
+
# music_player_html = create_placeholder_html(
|
| 1000 |
+
# track.get('title', 'Unknown'),
|
| 1001 |
+
# track.get('artist', 'Unknown'),
|
| 1002 |
+
# delay_s,
|
| 1003 |
+
# "music"
|
| 1004 |
+
# )
|
| 1005 |
+
# print(f"β
Voice request YouTube player scheduled - iframe added after {delay_s:.1f}s")
|
| 1006 |
+
# print(f"β
YouTube player scheduled: {track.get('title', 'Unknown')} (ID: {youtube_id})")
|
| 1007 |
+
|
| 1008 |
+
# Simple immediate iframe - start track with host
|
| 1009 |
+
music_player_html = create_youtube_player_html(
|
| 1010 |
+
youtube_id,
|
| 1011 |
+
track.get('title', 'Unknown'),
|
| 1012 |
+
track.get('artist', 'Unknown'),
|
| 1013 |
+
track['url']
|
| 1014 |
+
)
|
| 1015 |
+
print(f"β
Voice request YouTube player created immediately: {track.get('title', 'Unknown')} (ID: {youtube_id})")
|
| 1016 |
else:
|
| 1017 |
music_player_html = f"""
|
| 1018 |
<div style="padding: 1rem; background: #f0f0f0; border-radius: 10px; margin: 1rem 0;">
|
|
|
|
| 1278 |
}
|
| 1279 |
"""
|
| 1280 |
|
| 1281 |
+
# Auto-play JavaScript (injected in head)
|
| 1282 |
+
auto_play_head = """
|
| 1283 |
+
<script>
|
| 1284 |
+
(function() {
|
| 1285 |
+
let autoPlayEnabled = true;
|
| 1286 |
+
let nextSegmentTimeout = null;
|
| 1287 |
+
let currentAudioElement = null;
|
| 1288 |
+
let musicPlayDuration = 60000; // Wait 60 seconds for music to play
|
| 1289 |
+
|
| 1290 |
+
function clickNextSegment() {
|
| 1291 |
+
const buttons = document.querySelectorAll('button');
|
| 1292 |
+
for (let btn of buttons) {
|
| 1293 |
+
if (btn.textContent.includes('Next Segment')) {
|
| 1294 |
+
console.log('βοΈ Auto-clicking Next Segment button...');
|
| 1295 |
+
btn.click();
|
| 1296 |
+
return true;
|
| 1297 |
+
}
|
| 1298 |
+
}
|
| 1299 |
+
return false;
|
| 1300 |
+
}
|
| 1301 |
+
|
| 1302 |
+
function onAudioEnded() {
|
| 1303 |
+
if (!autoPlayEnabled) return;
|
| 1304 |
+
|
| 1305 |
+
console.log('π΅ Host audio ended, scheduling next segment in ' + (musicPlayDuration/1000) + 's...');
|
| 1306 |
+
|
| 1307 |
+
// Clear any existing timeout
|
| 1308 |
+
if (nextSegmentTimeout) clearTimeout(nextSegmentTimeout);
|
| 1309 |
+
|
| 1310 |
+
// Wait for music/podcast to play, then auto-advance
|
| 1311 |
+
nextSegmentTimeout = setTimeout(function() {
|
| 1312 |
+
clickNextSegment();
|
| 1313 |
+
}, musicPlayDuration);
|
| 1314 |
+
}
|
| 1315 |
+
|
| 1316 |
+
function setupAutoPlay() {
|
| 1317 |
+
// Find all audio elements in the host_audio container
|
| 1318 |
+
const audioContainer = document.querySelector('#host_audio');
|
| 1319 |
+
if (!audioContainer) {
|
| 1320 |
+
return;
|
| 1321 |
+
}
|
| 1322 |
+
|
| 1323 |
+
const audio = audioContainer.querySelector('audio');
|
| 1324 |
+
if (!audio) {
|
| 1325 |
+
return;
|
| 1326 |
+
}
|
| 1327 |
+
|
| 1328 |
+
// If this is a different audio element than before, set up new listener
|
| 1329 |
+
if (audio !== currentAudioElement) {
|
| 1330 |
+
console.log('π New audio element detected, setting up auto-play...');
|
| 1331 |
+
currentAudioElement = audio;
|
| 1332 |
+
|
| 1333 |
+
// Remove old listener if exists (defensive)
|
| 1334 |
+
audio.removeEventListener('ended', onAudioEnded);
|
| 1335 |
+
|
| 1336 |
+
// Add new listener
|
| 1337 |
+
audio.addEventListener('ended', onAudioEnded);
|
| 1338 |
+
|
| 1339 |
+
console.log('β
Auto-play listener attached to audio element');
|
| 1340 |
+
}
|
| 1341 |
+
}
|
| 1342 |
+
|
| 1343 |
+
// Set up on load
|
| 1344 |
+
if (document.readyState === 'loading') {
|
| 1345 |
+
document.addEventListener('DOMContentLoaded', function() {
|
| 1346 |
+
setupAutoPlay();
|
| 1347 |
+
});
|
| 1348 |
+
} else {
|
| 1349 |
+
setupAutoPlay();
|
| 1350 |
+
}
|
| 1351 |
+
|
| 1352 |
+
// Re-setup when DOM changes (Gradio updates audio element)
|
| 1353 |
+
const observer = new MutationObserver(function(mutations) {
|
| 1354 |
+
// Check if audio-related changes
|
| 1355 |
+
for (let mutation of mutations) {
|
| 1356 |
+
if (mutation.type === 'childList') {
|
| 1357 |
+
// Look for audio element changes
|
| 1358 |
+
const hasAudioChange = Array.from(mutation.addedNodes).some(node =>
|
| 1359 |
+
node.nodeType === 1 && (node.tagName === 'AUDIO' || node.querySelector && node.querySelector('audio'))
|
| 1360 |
+
);
|
| 1361 |
+
if (hasAudioChange || mutation.target.id === 'host_audio' ||
|
| 1362 |
+
(mutation.target.closest && mutation.target.closest('#host_audio'))) {
|
| 1363 |
+
setupAutoPlay();
|
| 1364 |
+
break;
|
| 1365 |
+
}
|
| 1366 |
+
}
|
| 1367 |
+
}
|
| 1368 |
+
});
|
| 1369 |
+
|
| 1370 |
+
// Start observing after a short delay to let Gradio initialize
|
| 1371 |
+
setTimeout(function() {
|
| 1372 |
+
observer.observe(document.body, { childList: true, subtree: true });
|
| 1373 |
+
setupAutoPlay(); // Initial setup
|
| 1374 |
+
|
| 1375 |
+
// Also periodically check (backup)
|
| 1376 |
+
setInterval(setupAutoPlay, 5000);
|
| 1377 |
+
}, 2000);
|
| 1378 |
+
|
| 1379 |
+
// Expose functions globally
|
| 1380 |
+
window.toggleAutoPlay = function(enabled) {
|
| 1381 |
+
autoPlayEnabled = enabled;
|
| 1382 |
+
console.log('Auto-play ' + (enabled ? 'enabled' : 'disabled'));
|
| 1383 |
+
};
|
| 1384 |
+
|
| 1385 |
+
window.setMusicPlayDuration = function(ms) {
|
| 1386 |
+
musicPlayDuration = ms;
|
| 1387 |
+
console.log('Music play duration set to ' + (ms/1000) + 's');
|
| 1388 |
+
};
|
| 1389 |
+
|
| 1390 |
+
window.cancelNextSegment = function() {
|
| 1391 |
+
if (nextSegmentTimeout) {
|
| 1392 |
+
clearTimeout(nextSegmentTimeout);
|
| 1393 |
+
nextSegmentTimeout = null;
|
| 1394 |
+
console.log('βΉοΈ Next segment cancelled');
|
| 1395 |
+
}
|
| 1396 |
+
};
|
| 1397 |
+
|
| 1398 |
+
window.playNextNow = function() {
|
| 1399 |
+
if (nextSegmentTimeout) clearTimeout(nextSegmentTimeout);
|
| 1400 |
+
clickNextSegment();
|
| 1401 |
+
};
|
| 1402 |
+
})();
|
| 1403 |
+
</script>
|
| 1404 |
+
"""
|
| 1405 |
+
|
| 1406 |
# Build Gradio Interface
|
| 1407 |
+
with gr.Blocks(css=custom_css, title="AI Radio π΅", theme=gr.themes.Soft(), head=auto_play_head) as demo:
|
| 1408 |
|
| 1409 |
# Hidden state for user ID (persisted via localStorage)
|
| 1410 |
user_id_state = gr.State(value=None)
|
|
|
|
| 1561 |
audio_output = gr.Audio(label="π Host Speech", autoplay=True, type="filepath", elem_id="host_audio")
|
| 1562 |
music_player = gr.HTML(label="π΅ Music/Podcast Player (streaming)")
|
| 1563 |
|
| 1564 |
+
# # COMMENTED OUT: Timer to check for pending players (delayed loading mechanism)
|
| 1565 |
+
# # Timer to check for pending players (polls every 2 seconds)
|
| 1566 |
+
# player_timer = gr.Timer(value=2, active=True)
|
| 1567 |
+
|
| 1568 |
+
# def check_pending_player():
|
| 1569 |
+
# """Check if there's a pending player ready to show"""
|
| 1570 |
+
# pending = radio_state.get("pending_player")
|
| 1571 |
+
# if pending and time.time() >= pending.get("ready_at", 0):
|
| 1572 |
+
# html = pending.get("html", "")
|
| 1573 |
+
# title = pending.get("title", "Unknown")
|
| 1574 |
+
# radio_state["pending_player"] = None # Clear it
|
| 1575 |
+
# print(f"βΆοΈ Adding iframe for: {title}")
|
| 1576 |
+
# return html
|
| 1577 |
+
# return gr.update() # No change
|
| 1578 |
+
|
| 1579 |
+
# player_timer.tick(
|
| 1580 |
+
# fn=check_pending_player,
|
| 1581 |
+
# inputs=[],
|
| 1582 |
+
# outputs=[music_player]
|
| 1583 |
+
# )
|
| 1584 |
+
|
| 1585 |
status_text = gr.Textbox(label="Status", value="Ready", interactive=False, visible=False)
|
| 1586 |
|
| 1587 |
# Connect buttons
|
|
|
|
| 1600 |
stop_btn.click(
|
| 1601 |
fn=stop_radio,
|
| 1602 |
inputs=[],
|
| 1603 |
+
outputs=[status_text, audio_output, music_player, progress_text, now_playing],
|
| 1604 |
+
js="() => { if(window.cancelNextSegment) window.cancelNextSegment(); }"
|
| 1605 |
)
|
| 1606 |
|
| 1607 |
# Voice input button - direct click without .then() chain
|
|
|
|
| 1878 |
if __name__ == "__main__":
|
| 1879 |
demo.launch(
|
| 1880 |
server_name="0.0.0.0",
|
| 1881 |
+
server_port=7871,
|
| 1882 |
share=False
|
| 1883 |
)
|
| 1884 |
|
src/mcp_servers/music_server.py
CHANGED
|
@@ -26,6 +26,34 @@ class MusicMCPServer:
|
|
| 26 |
os.makedirs(self.cache_dir, exist_ok=True)
|
| 27 |
# Cache for embed check results to avoid repeated API calls
|
| 28 |
self._embed_cache = {}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 29 |
|
| 30 |
def check_video_embeddable(self, video_id: str) -> bool:
|
| 31 |
"""
|
|
@@ -95,7 +123,7 @@ class MusicMCPServer:
|
|
| 95 |
self._embed_cache[video_id] = False
|
| 96 |
return False
|
| 97 |
|
| 98 |
-
def search_youtube_music(self, query: str, limit: int = 5, fast: bool = False, check_embed: bool =
|
| 99 |
"""
|
| 100 |
Search for free music on YouTube
|
| 101 |
|
|
@@ -103,11 +131,14 @@ class MusicMCPServer:
|
|
| 103 |
query: Search query (e.g., "pop music", "jazz instrumental", "song name")
|
| 104 |
limit: Number of results to fetch (will randomly select one)
|
| 105 |
fast: If True, use flat extraction for faster results (less metadata)
|
| 106 |
-
check_embed: If True, verify videos are embeddable (
|
| 107 |
|
| 108 |
Returns:
|
| 109 |
List of track dictionaries with YouTube URLs
|
| 110 |
"""
|
|
|
|
|
|
|
|
|
|
| 111 |
tracks = []
|
| 112 |
try:
|
| 113 |
# Use extract_flat for faster search (no full video info)
|
|
@@ -119,7 +150,8 @@ class MusicMCPServer:
|
|
| 119 |
}
|
| 120 |
|
| 121 |
# Search for more results to allow for filtering and random selection
|
| 122 |
-
|
|
|
|
| 123 |
|
| 124 |
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
|
| 125 |
# Don't add "music" if query already contains it or is specific
|
|
@@ -155,14 +187,19 @@ class MusicMCPServer:
|
|
| 155 |
if len(valid_entries) > 1:
|
| 156 |
random.shuffle(valid_entries)
|
| 157 |
|
| 158 |
-
#
|
| 159 |
for entry in valid_entries:
|
| 160 |
if len(tracks) >= limit:
|
| 161 |
break
|
| 162 |
|
| 163 |
video_id = entry.get('id') or entry.get('url', '')
|
| 164 |
if video_id:
|
| 165 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 166 |
if check_embed and not self.check_video_embeddable(video_id):
|
| 167 |
print(f" β Skipping non-embeddable: {entry.get('title', 'Unknown')}")
|
| 168 |
continue
|
|
@@ -177,7 +214,9 @@ class MusicMCPServer:
|
|
| 177 |
"source": "youtube"
|
| 178 |
}
|
| 179 |
tracks.append(track)
|
| 180 |
-
|
|
|
|
|
|
|
| 181 |
|
| 182 |
except Exception as e:
|
| 183 |
print(f"β Error searching YouTube: {e}")
|
|
|
|
| 26 |
os.makedirs(self.cache_dir, exist_ok=True)
|
| 27 |
# Cache for embed check results to avoid repeated API calls
|
| 28 |
self._embed_cache = {}
|
| 29 |
+
# Rate limiting for YouTube API (prevent blocking)
|
| 30 |
+
self._last_youtube_call = 0
|
| 31 |
+
self._min_call_interval = 3.0 # Minimum 3 seconds between YouTube calls
|
| 32 |
+
# Track recently played to avoid repeats
|
| 33 |
+
self._recently_played = [] # List of video IDs
|
| 34 |
+
self._max_recent = 20 # Remember last 20 tracks
|
| 35 |
+
|
| 36 |
+
def _rate_limit_youtube(self):
|
| 37 |
+
"""Enforce rate limiting for YouTube API calls"""
|
| 38 |
+
import time as time_module
|
| 39 |
+
current_time = time_module.time()
|
| 40 |
+
elapsed = current_time - self._last_youtube_call
|
| 41 |
+
if elapsed < self._min_call_interval:
|
| 42 |
+
sleep_time = self._min_call_interval - elapsed
|
| 43 |
+
print(f"β³ Rate limiting: waiting {sleep_time:.1f}s before YouTube call...")
|
| 44 |
+
time_module.sleep(sleep_time)
|
| 45 |
+
self._last_youtube_call = time_module.time()
|
| 46 |
+
|
| 47 |
+
def _add_to_recently_played(self, video_id: str):
|
| 48 |
+
"""Track a video as recently played"""
|
| 49 |
+
if video_id and video_id not in self._recently_played:
|
| 50 |
+
self._recently_played.append(video_id)
|
| 51 |
+
if len(self._recently_played) > self._max_recent:
|
| 52 |
+
self._recently_played.pop(0)
|
| 53 |
+
|
| 54 |
+
def _is_recently_played(self, video_id: str) -> bool:
|
| 55 |
+
"""Check if a video was recently played"""
|
| 56 |
+
return video_id in self._recently_played
|
| 57 |
|
| 58 |
def check_video_embeddable(self, video_id: str) -> bool:
|
| 59 |
"""
|
|
|
|
| 123 |
self._embed_cache[video_id] = False
|
| 124 |
return False
|
| 125 |
|
| 126 |
+
def search_youtube_music(self, query: str, limit: int = 5, fast: bool = False, check_embed: bool = False) -> List[Dict[str, Any]]:
|
| 127 |
"""
|
| 128 |
Search for free music on YouTube
|
| 129 |
|
|
|
|
| 131 |
query: Search query (e.g., "pop music", "jazz instrumental", "song name")
|
| 132 |
limit: Number of results to fetch (will randomly select one)
|
| 133 |
fast: If True, use flat extraction for faster results (less metadata)
|
| 134 |
+
check_embed: If True, verify videos are embeddable (slower but more reliable)
|
| 135 |
|
| 136 |
Returns:
|
| 137 |
List of track dictionaries with YouTube URLs
|
| 138 |
"""
|
| 139 |
+
# Apply rate limiting
|
| 140 |
+
self._rate_limit_youtube()
|
| 141 |
+
|
| 142 |
tracks = []
|
| 143 |
try:
|
| 144 |
# Use extract_flat for faster search (no full video info)
|
|
|
|
| 150 |
}
|
| 151 |
|
| 152 |
# Search for more results to allow for filtering and random selection
|
| 153 |
+
# Increase limit to account for filtering out recently played
|
| 154 |
+
search_limit = max(limit * 3, 15)
|
| 155 |
|
| 156 |
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
|
| 157 |
# Don't add "music" if query already contains it or is specific
|
|
|
|
| 187 |
if len(valid_entries) > 1:
|
| 188 |
random.shuffle(valid_entries)
|
| 189 |
|
| 190 |
+
# Filter, check embeddability, avoid recently played, and take requested limit
|
| 191 |
for entry in valid_entries:
|
| 192 |
if len(tracks) >= limit:
|
| 193 |
break
|
| 194 |
|
| 195 |
video_id = entry.get('id') or entry.get('url', '')
|
| 196 |
if video_id:
|
| 197 |
+
# Skip recently played tracks
|
| 198 |
+
if self._is_recently_played(video_id):
|
| 199 |
+
print(f" β Skipping recently played: {entry.get('title', 'Unknown')}")
|
| 200 |
+
continue
|
| 201 |
+
|
| 202 |
+
# Check if video is embeddable (optional)
|
| 203 |
if check_embed and not self.check_video_embeddable(video_id):
|
| 204 |
print(f" β Skipping non-embeddable: {entry.get('title', 'Unknown')}")
|
| 205 |
continue
|
|
|
|
| 214 |
"source": "youtube"
|
| 215 |
}
|
| 216 |
tracks.append(track)
|
| 217 |
+
# Mark as recently played
|
| 218 |
+
self._add_to_recently_played(video_id)
|
| 219 |
+
print(f" β Found: {track['title']} by {track['artist']}")
|
| 220 |
|
| 221 |
except Exception as e:
|
| 222 |
print(f"β Error searching YouTube: {e}")
|
src/mcp_servers/podcast_server.py
CHANGED
|
@@ -18,6 +18,23 @@ class PodcastMCPServer:
|
|
| 18 |
self.name = "podcast_server"
|
| 19 |
self.description = "Provides podcast recommendations from YouTube"
|
| 20 |
self._embed_cache = {}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
|
| 22 |
def check_video_embeddable(self, video_id: str) -> bool:
|
| 23 |
"""Check if a YouTube video is embeddable"""
|
|
@@ -85,6 +102,9 @@ class PodcastMCPServer:
|
|
| 85 |
print("β οΈ yt-dlp not available, using demo podcasts")
|
| 86 |
return self._get_demo_podcasts(category, limit)
|
| 87 |
|
|
|
|
|
|
|
|
|
|
| 88 |
try:
|
| 89 |
# Build search query for podcasts
|
| 90 |
search_query = f"{query} podcast {category}"
|
|
|
|
| 18 |
self.name = "podcast_server"
|
| 19 |
self.description = "Provides podcast recommendations from YouTube"
|
| 20 |
self._embed_cache = {}
|
| 21 |
+
# Rate limiting
|
| 22 |
+
self._last_youtube_call = 0
|
| 23 |
+
self._min_call_interval = 3.0 # Minimum 3 seconds between YouTube calls
|
| 24 |
+
# Track recently played podcasts
|
| 25 |
+
self._recently_played = []
|
| 26 |
+
self._max_recent = 10
|
| 27 |
+
|
| 28 |
+
def _rate_limit_youtube(self):
|
| 29 |
+
"""Enforce rate limiting for YouTube API calls"""
|
| 30 |
+
import time as time_module
|
| 31 |
+
current_time = time_module.time()
|
| 32 |
+
elapsed = current_time - self._last_youtube_call
|
| 33 |
+
if elapsed < self._min_call_interval:
|
| 34 |
+
sleep_time = self._min_call_interval - elapsed
|
| 35 |
+
print(f"β³ Podcast rate limiting: waiting {sleep_time:.1f}s...")
|
| 36 |
+
time_module.sleep(sleep_time)
|
| 37 |
+
self._last_youtube_call = time_module.time()
|
| 38 |
|
| 39 |
def check_video_embeddable(self, video_id: str) -> bool:
|
| 40 |
"""Check if a YouTube video is embeddable"""
|
|
|
|
| 102 |
print("β οΈ yt-dlp not available, using demo podcasts")
|
| 103 |
return self._get_demo_podcasts(category, limit)
|
| 104 |
|
| 105 |
+
# Apply rate limiting
|
| 106 |
+
self._rate_limit_youtube()
|
| 107 |
+
|
| 108 |
try:
|
| 109 |
# Build search query for podcasts
|
| 110 |
search_query = f"{query} podcast {category}"
|
src/radio_agent.py
CHANGED
|
@@ -11,6 +11,7 @@ from mcp_servers.music_server import MusicMCPServer
|
|
| 11 |
from mcp_servers.news_server import NewsMCPServer
|
| 12 |
from mcp_servers.podcast_server import PodcastMCPServer
|
| 13 |
from rag_system import RadioRAGSystem
|
|
|
|
| 14 |
|
| 15 |
# -----------------------------------------------------------------------------
|
| 16 |
# LLM Logging Setup
|
|
@@ -218,7 +219,7 @@ class RadioAgent:
|
|
| 218 |
model=self.config.nebius_model,
|
| 219 |
messages=[{"role": "user", "content": prompt}],
|
| 220 |
temperature=0.9,
|
| 221 |
-
max_tokens=
|
| 222 |
)
|
| 223 |
text = response.choices[0].message.content.strip()
|
| 224 |
llm_logger.info(
|
|
@@ -248,7 +249,7 @@ class RadioAgent:
|
|
| 248 |
model=self.config.nebius_model,
|
| 249 |
messages=[{"role": "user", "content": prompt}],
|
| 250 |
temperature=0.9,
|
| 251 |
-
max_tokens=
|
| 252 |
)
|
| 253 |
text = response.choices[0].message.content.strip()
|
| 254 |
llm_logger.info(
|
|
@@ -263,19 +264,75 @@ class RadioAgent:
|
|
| 263 |
|
| 264 |
return f"That's all for now, {name}! Thanks for tuning in to AI Radio. Come back soon for more personalized content!"
|
| 265 |
|
| 266 |
-
def _plan_music_segment(self, preferences: Dict[str, Any]) -> Dict[str, Any]:
|
| 267 |
-
"""Plan a music segment using Music MCP Server
|
| 268 |
-
|
| 269 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 270 |
|
| 271 |
-
# Use
|
| 272 |
-
|
| 273 |
-
|
| 274 |
-
mood=mood,
|
| 275 |
-
limit=1
|
| 276 |
-
)
|
| 277 |
|
| 278 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 279 |
|
| 280 |
return {
|
| 281 |
'type': 'music',
|
|
@@ -375,7 +432,7 @@ class RadioAgent:
|
|
| 375 |
model=self.config.nebius_model,
|
| 376 |
messages=[{"role": "user", "content": prompt}],
|
| 377 |
temperature=0.9,
|
| 378 |
-
max_tokens=
|
| 379 |
)
|
| 380 |
text = response.choices[0].message.content.strip()
|
| 381 |
|
|
@@ -419,7 +476,7 @@ class RadioAgent:
|
|
| 419 |
# Generate full news script (1-2 minutes of speech)
|
| 420 |
news_text = "\n".join([f"- {item['title']}: {item['summary']}" for item in news_items[:3]])
|
| 421 |
prompt = f"""You are a professional radio news presenter named Lera. Say that this is news time on the show.
|
| 422 |
-
Present these news items in a conversational, engaging way. This should be about 1 minute of speech (about 200-250 words, max_tokens =
|
| 423 |
|
| 424 |
News text:
|
| 425 |
{news_text}
|
|
@@ -438,7 +495,7 @@ class RadioAgent:
|
|
| 438 |
model=self.config.nebius_model,
|
| 439 |
messages=[{"role": "user", "content": prompt}],
|
| 440 |
temperature=0.9,
|
| 441 |
-
max_tokens=
|
| 442 |
)
|
| 443 |
full_script = response.choices[0].message.content.strip()
|
| 444 |
llm_logger.info(
|
|
@@ -492,7 +549,7 @@ class RadioAgent:
|
|
| 492 |
model=self.config.nebius_model,
|
| 493 |
messages=[{"role": "user", "content": prompt}],
|
| 494 |
temperature=0.8,
|
| 495 |
-
max_tokens=
|
| 496 |
)
|
| 497 |
text = response.choices[0].message.content.strip()
|
| 498 |
llm_logger.info(
|
|
@@ -519,7 +576,7 @@ class RadioAgent:
|
|
| 519 |
model=self.config.nebius_model,
|
| 520 |
messages=[{"role": "user", "content": prompt}],
|
| 521 |
temperature=0.9,
|
| 522 |
-
max_tokens=
|
| 523 |
)
|
| 524 |
text = response.choices[0].message.content.strip()
|
| 525 |
llm_logger.info(
|
|
@@ -553,7 +610,7 @@ Don't use emojis. Just speak naturally like a real DJ."""
|
|
| 553 |
model=self.config.nebius_model,
|
| 554 |
messages=[{"role": "user", "content": prompt}],
|
| 555 |
temperature=0.9,
|
| 556 |
-
max_tokens=
|
| 557 |
)
|
| 558 |
text = response.choices[0].message.content.strip()
|
| 559 |
llm_logger.info(
|
|
@@ -655,12 +712,14 @@ Don't use emojis. Just speak naturally like a real DJ."""
|
|
| 655 |
else:
|
| 656 |
print(f"π΅ [RAG] Using genre: {selected_genre} (from preferences)")
|
| 657 |
|
| 658 |
-
|
| 659 |
-
|
| 660 |
-
|
| 661 |
-
|
| 662 |
-
|
| 663 |
-
|
|
|
|
|
|
|
| 664 |
|
| 665 |
track = segment.get("track")
|
| 666 |
if track and not segment.get("commentary"):
|
|
|
|
| 11 |
from mcp_servers.news_server import NewsMCPServer
|
| 12 |
from mcp_servers.podcast_server import PodcastMCPServer
|
| 13 |
from rag_system import RadioRAGSystem
|
| 14 |
+
from user_memory import UserMemoryService
|
| 15 |
|
| 16 |
# -----------------------------------------------------------------------------
|
| 17 |
# LLM Logging Setup
|
|
|
|
| 219 |
model=self.config.nebius_model,
|
| 220 |
messages=[{"role": "user", "content": prompt}],
|
| 221 |
temperature=0.9,
|
| 222 |
+
max_tokens=100 #200
|
| 223 |
)
|
| 224 |
text = response.choices[0].message.content.strip()
|
| 225 |
llm_logger.info(
|
|
|
|
| 249 |
model=self.config.nebius_model,
|
| 250 |
messages=[{"role": "user", "content": prompt}],
|
| 251 |
temperature=0.9,
|
| 252 |
+
max_tokens=100 #200
|
| 253 |
)
|
| 254 |
text = response.choices[0].message.content.strip()
|
| 255 |
llm_logger.info(
|
|
|
|
| 264 |
|
| 265 |
return f"That's all for now, {name}! Thanks for tuning in to AI Radio. Come back soon for more personalized content!"
|
| 266 |
|
| 267 |
+
def _plan_music_segment(self, preferences: Dict[str, Any], user_id: str = None) -> Dict[str, Any]:
|
| 268 |
+
"""Plan a music segment using Music MCP Server with RAG preferences"""
|
| 269 |
+
# Get user preferences from RAG (more accurate than just preferences dict)
|
| 270 |
+
rag_prefs = {}
|
| 271 |
+
if user_id and self.rag_system:
|
| 272 |
+
try:
|
| 273 |
+
rag_prefs = self.rag_system.get_user_preferences(user_id)
|
| 274 |
+
print(f"π΅ [RAG] Retrieved preferences for user {user_id}: genres={rag_prefs.get('favorite_genres', [])}")
|
| 275 |
+
except Exception as e:
|
| 276 |
+
print(f"β οΈ [RAG] Could not get preferences: {e}")
|
| 277 |
|
| 278 |
+
# Use RAG preferences if available, otherwise fall back to provided preferences
|
| 279 |
+
genres = rag_prefs.get('favorite_genres') or preferences.get('favorite_genres', ['pop'])
|
| 280 |
+
mood = rag_prefs.get('mood') or preferences.get('mood', 'happy')
|
|
|
|
|
|
|
|
|
|
| 281 |
|
| 282 |
+
# Get recently played tracks from user memory to avoid duplicates
|
| 283 |
+
played_video_ids = set()
|
| 284 |
+
if user_id:
|
| 285 |
+
try:
|
| 286 |
+
user_memory = UserMemoryService()
|
| 287 |
+
play_history = user_memory.get_play_history(user_id)
|
| 288 |
+
# Extract YouTube IDs from history
|
| 289 |
+
for entry in play_history:
|
| 290 |
+
if entry.get('youtube_id'):
|
| 291 |
+
played_video_ids.add(entry['youtube_id'])
|
| 292 |
+
elif entry.get('url') and 'youtube.com' in entry.get('url', ''):
|
| 293 |
+
# Extract ID from URL
|
| 294 |
+
url = entry['url']
|
| 295 |
+
if 'v=' in url:
|
| 296 |
+
vid_id = url.split('v=')[-1].split('&')[0]
|
| 297 |
+
played_video_ids.add(vid_id)
|
| 298 |
+
print(f"π΅ [RAG] Found {len(played_video_ids)} previously played tracks to avoid")
|
| 299 |
+
except Exception as e:
|
| 300 |
+
print(f"β οΈ Could not get play history: {e}")
|
| 301 |
+
|
| 302 |
+
# Search for music, filtering out played tracks
|
| 303 |
+
max_attempts = 5
|
| 304 |
+
track = None
|
| 305 |
+
for attempt in range(max_attempts):
|
| 306 |
+
# Use MCP server to get music
|
| 307 |
+
tracks = self.music_server.search_free_music(
|
| 308 |
+
genre=random.choice(genres) if genres else 'pop',
|
| 309 |
+
mood=mood,
|
| 310 |
+
limit=10 # Get more results to filter from
|
| 311 |
+
)
|
| 312 |
+
|
| 313 |
+
# Filter out already played tracks
|
| 314 |
+
available_tracks = [
|
| 315 |
+
t for t in tracks
|
| 316 |
+
if t.get('youtube_id') and t['youtube_id'] not in played_video_ids
|
| 317 |
+
]
|
| 318 |
+
|
| 319 |
+
if available_tracks:
|
| 320 |
+
track = random.choice(available_tracks)
|
| 321 |
+
# Mark as recently played in music server to avoid duplicates
|
| 322 |
+
if track.get('youtube_id'):
|
| 323 |
+
self.music_server._add_to_recently_played(track['youtube_id'])
|
| 324 |
+
print(f"β
[RAG] Selected new track: {track.get('title', 'Unknown')} (not in history)")
|
| 325 |
+
break
|
| 326 |
+
else:
|
| 327 |
+
print(f"β οΈ [RAG] All tracks already played, trying different genre...")
|
| 328 |
+
# Try a different genre if all tracks were played
|
| 329 |
+
if attempt < max_attempts - 1:
|
| 330 |
+
genres = [g for g in genres if g != random.choice(genres)] or ['pop']
|
| 331 |
+
|
| 332 |
+
# If still no track, use any available (fallback)
|
| 333 |
+
if not track and tracks:
|
| 334 |
+
track = tracks[0]
|
| 335 |
+
print(f"β οΈ Using track despite possible duplicate: {track.get('title', 'Unknown')}")
|
| 336 |
|
| 337 |
return {
|
| 338 |
'type': 'music',
|
|
|
|
| 432 |
model=self.config.nebius_model,
|
| 433 |
messages=[{"role": "user", "content": prompt}],
|
| 434 |
temperature=0.9,
|
| 435 |
+
max_tokens=100 #400
|
| 436 |
)
|
| 437 |
text = response.choices[0].message.content.strip()
|
| 438 |
|
|
|
|
| 476 |
# Generate full news script (1-2 minutes of speech)
|
| 477 |
news_text = "\n".join([f"- {item['title']}: {item['summary']}" for item in news_items[:3]])
|
| 478 |
prompt = f"""You are a professional radio news presenter named Lera. Say that this is news time on the show.
|
| 479 |
+
Present these news items in a conversational, engaging way. This should be about 1 minute of speech (about 200-250 words, max_tokens = 400):
|
| 480 |
|
| 481 |
News text:
|
| 482 |
{news_text}
|
|
|
|
| 495 |
model=self.config.nebius_model,
|
| 496 |
messages=[{"role": "user", "content": prompt}],
|
| 497 |
temperature=0.9,
|
| 498 |
+
max_tokens=100 #400
|
| 499 |
)
|
| 500 |
full_script = response.choices[0].message.content.strip()
|
| 501 |
llm_logger.info(
|
|
|
|
| 549 |
model=self.config.nebius_model,
|
| 550 |
messages=[{"role": "user", "content": prompt}],
|
| 551 |
temperature=0.8,
|
| 552 |
+
max_tokens=100 #200
|
| 553 |
)
|
| 554 |
text = response.choices[0].message.content.strip()
|
| 555 |
llm_logger.info(
|
|
|
|
| 576 |
model=self.config.nebius_model,
|
| 577 |
messages=[{"role": "user", "content": prompt}],
|
| 578 |
temperature=0.9,
|
| 579 |
+
max_tokens=100 #600
|
| 580 |
)
|
| 581 |
text = response.choices[0].message.content.strip()
|
| 582 |
llm_logger.info(
|
|
|
|
| 610 |
model=self.config.nebius_model,
|
| 611 |
messages=[{"role": "user", "content": prompt}],
|
| 612 |
temperature=0.9,
|
| 613 |
+
max_tokens=100 #200 # Keep it short for fast response
|
| 614 |
)
|
| 615 |
text = response.choices[0].message.content.strip()
|
| 616 |
llm_logger.info(
|
|
|
|
| 712 |
else:
|
| 713 |
print(f"π΅ [RAG] Using genre: {selected_genre} (from preferences)")
|
| 714 |
|
| 715 |
+
# Update preferences with RAG-suggested genre
|
| 716 |
+
updated_prefs = prefs.copy()
|
| 717 |
+
if selected_genre not in updated_prefs.get('favorite_genres', []):
|
| 718 |
+
updated_prefs['favorite_genres'] = updated_prefs.get('favorite_genres', []) + [selected_genre]
|
| 719 |
+
|
| 720 |
+
# Use _plan_music_segment which handles RAG preferences and duplicate checking
|
| 721 |
+
music_segment = self._plan_music_segment(updated_prefs, user_id=user_id)
|
| 722 |
+
segment["track"] = music_segment.get("track")
|
| 723 |
|
| 724 |
track = segment.get("track")
|
| 725 |
if track and not segment.get("commentary"):
|
src/rag_system.py
CHANGED
|
@@ -541,8 +541,12 @@ Feedback: {user_feedback or 'No feedback'}
|
|
| 541 |
|
| 542 |
# Log each retrieved document
|
| 543 |
preview = node.text[:100].replace('\n', ' ')
|
| 544 |
-
|
| 545 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 546 |
|
| 547 |
# Stop if we have enough results for this user
|
| 548 |
if len(results) >= top_k:
|
|
@@ -561,8 +565,12 @@ Feedback: {user_feedback or 'No feedback'}
|
|
| 561 |
"metadata": node.metadata
|
| 562 |
}
|
| 563 |
results.append(result)
|
| 564 |
-
|
| 565 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 566 |
|
| 567 |
if len(results) >= top_k:
|
| 568 |
break
|
|
|
|
| 541 |
|
| 542 |
# Log each retrieved document
|
| 543 |
preview = node.text[:100].replace('\n', ' ')
|
| 544 |
+
try:
|
| 545 |
+
score_str = f"{float(score):.4f}" if score is not None else "N/A"
|
| 546 |
+
except (TypeError, ValueError):
|
| 547 |
+
score_str = str(score) if score is not None else "N/A"
|
| 548 |
+
rag_logger.info(f" π Retrieved #{len(results)}: user_id={node_user_id}, type={node_type}, item_type={item_type}, score={score_str}, preview='{preview}...'")
|
| 549 |
+
print(f" π [RAG] Retrieved #{len(results)}: {node_type} (user: {node_user_id}, score: {score_str}) - {preview}...")
|
| 550 |
|
| 551 |
# Stop if we have enough results for this user
|
| 552 |
if len(results) >= top_k:
|
|
|
|
| 565 |
"metadata": node.metadata
|
| 566 |
}
|
| 567 |
results.append(result)
|
| 568 |
+
try:
|
| 569 |
+
score_str = f"{float(score):.4f}" if score is not None else "N/A"
|
| 570 |
+
except (TypeError, ValueError):
|
| 571 |
+
score_str = str(score) if score is not None else "N/A"
|
| 572 |
+
rag_logger.warning(f" β οΈ Retrieved #{len(results)} (parse error): user_id={node_user_id}, score={score_str}, text_preview='{node.text[:50]}...'")
|
| 573 |
+
print(f" β οΈ [RAG] Retrieved #{len(results)} (parse error, user: {node_user_id}, score: {score_str})")
|
| 574 |
|
| 575 |
if len(results) >= top_k:
|
| 576 |
break
|