Nikita Makarov commited on
Commit
c94876b
Β·
1 Parent(s): 5e41eee

v2.1 - works - but without syncs of voice and music.

Browse files
README.md CHANGED
@@ -147,7 +147,7 @@ cd src
147
  python app.py
148
  ```
149
 
150
- 5. Open your browser to `http://localhost:7863`
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=7870,
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
- - Plan the show (skeleton only, no LLM – but YouTube search can take a few seconds)
182
- - Immediately generate + play the first segment (LLM + TTS)
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["current_news_batches"] = []
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
- return "⏸️ Radio stopped", None, None, None, "", ""
 
 
 
 
 
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
- # Get host audio duration to calculate delay
420
- delay_ms = 0
421
- if host_audio_file and os.path.exists(host_audio_file):
422
- try:
423
- audio = AudioSegment.from_file(host_audio_file)
424
- duration_ms = len(audio)
425
- delay_ms = max(0, duration_ms - 3000) # Start 3 seconds before end
426
- print(f"⏱️ Host audio: {duration_ms}ms, YouTube delay: {delay_ms}ms")
427
- except Exception as e:
428
- print(f"⚠️ Could not get audio duration: {e}")
 
 
 
429
 
430
- # YouTube iframe with delayed display via CSS animation
431
- music_player_html = f"""
432
- <style>
433
- @keyframes showYoutube {{
434
- from {{ opacity: 0; }}
435
- to {{ opacity: 1; }}
436
- }}
437
- .youtube-delayed {{
438
- animation: showYoutube 0.5s ease-in forwards;
439
- animation-delay: {delay_ms}ms;
440
- opacity: 0;
441
- }}
442
- </style>
443
- <div style="padding: 1rem; background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%); border-radius: 12px; margin: 1rem 0; box-shadow: 0 4px 15px rgba(0,0,0,0.3);">
444
- <h4 style="margin: 0 0 0.75rem 0; color: #fff; font-size: 1.1em;">🎡 {track.get('title', 'Unknown')}</h4>
445
- <p style="margin: 0 0 0.75rem 0; color: #aaa; font-size: 0.9em;">by {track.get('artist', 'Unknown')}</p>
446
- <div class="youtube-delayed" style="position: relative; width: 100%; padding-bottom: 56.25%; border-radius: 8px; overflow: hidden; background: #000;">
447
- <iframe
448
- style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border: none;"
449
- src="https://www.youtube.com/embed/{youtube_id}?autoplay=1&rel=0&modestbranding=1&playsinline=1"
450
- allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
451
- allowfullscreen>
452
- </iframe>
453
- </div>
454
- <p style="margin-top: 0.75rem; font-size: 0.85em; margin-bottom: 0;">
455
- <a href="{track['url']}" target="_blank" style="color: #ff4757; text-decoration: none;">πŸ”— Open on YouTube</a>
456
- </p>
457
- </div>
458
- """
459
- print(f"βœ… YouTube player created: {track.get('title', 'Unknown')} (ID: {youtube_id}) - shows after {delay_ms}ms")
 
 
 
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
- # Get host audio duration to calculate delay
530
- delay_ms = 0
531
- if host_audio_file and os.path.exists(host_audio_file):
532
- try:
533
- audio = AudioSegment.from_file(host_audio_file)
534
- duration_ms = len(audio)
535
- delay_ms = max(0, duration_ms - 3000) # Start 3 seconds before end
536
- print(f"⏱️ Podcast host audio: {duration_ms}ms, YouTube delay: {delay_ms}ms")
537
- except Exception as e:
538
- print(f"⚠️ Could not get audio duration: {e}")
539
-
540
- # YouTube iframe with delayed display
541
- music_player_html = f"""
542
- <style>
543
- @keyframes showPodcast {{
544
- from {{ opacity: 0; }}
545
- to {{ opacity: 1; }}
546
- }}
547
- .podcast-delayed {{
548
- animation: showPodcast 0.5s ease-in forwards;
549
- animation-delay: {delay_ms}ms;
550
- opacity: 0;
551
- }}
552
- </style>
553
- <div style="padding: 1rem; background: linear-gradient(135deg, #6b46c1 0%, #9f7aea 100%); border-radius: 12px; margin: 1rem 0; box-shadow: 0 4px 15px rgba(0,0,0,0.3);">
554
- <h4 style="margin: 0 0 0.75rem 0; color: #fff; font-size: 1.1em;">πŸŽ™οΈ {podcast.get('title', 'Podcast')}</h4>
555
- <p style="margin: 0 0 0.75rem 0; color: #e9d8fd; font-size: 0.9em;">by {podcast.get('host', 'Unknown Host')}</p>
556
- <div class="podcast-delayed" style="position: relative; width: 100%; padding-bottom: 56.25%; border-radius: 8px; overflow: hidden; background: #000;">
557
- <iframe
558
- style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border: none;"
559
- src="https://www.youtube.com/embed/{youtube_id}?autoplay=1&rel=0&modestbranding=1&playsinline=1"
560
- allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
561
- allowfullscreen>
562
- </iframe>
563
- </div>
564
- <p style="margin-top: 0.75rem; font-size: 0.85em; margin-bottom: 0;">
565
- <a href="{podcast.get('url', '#')}" target="_blank" style="color: #e9d8fd; text-decoration: none;">πŸ”— Open on YouTube</a>
566
- </p>
567
- </div>
568
- """
569
- print(f"βœ… Podcast YouTube player created: {podcast.get('title', 'Unknown')} (ID: {youtube_id}) - shows after {delay_ms}ms")
 
 
 
 
 
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
- # Get host audio duration to calculate delay
581
- delay_ms = 0
582
- if host_audio_file and os.path.exists(host_audio_file):
583
- try:
584
- audio = AudioSegment.from_file(host_audio_file)
585
- duration_ms = len(audio)
586
- delay_ms = max(0, duration_ms - 3000)
587
- except:
588
- pass
 
589
 
590
- music_player_html = f"""
591
- <style>
592
- @keyframes showPodcast2 {{
593
- from {{ opacity: 0; }}
594
- to {{ opacity: 1; }}
595
- }}
596
- .podcast-delayed-2 {{
597
- animation: showPodcast2 0.5s ease-in forwards;
598
- animation-delay: {delay_ms}ms;
599
- opacity: 0;
600
- }}
601
- </style>
602
- <div style="padding: 1rem; background: linear-gradient(135deg, #6b46c1 0%, #9f7aea 100%); border-radius: 12px; margin: 1rem 0; box-shadow: 0 4px 15px rgba(0,0,0,0.3);">
603
- <h4 style="margin: 0 0 0.75rem 0; color: #fff; font-size: 1.1em;">πŸŽ™οΈ {podcast.get('title', 'Podcast')}</h4>
604
- <p style="margin: 0 0 0.75rem 0; color: #e9d8fd; font-size: 0.9em;">by {podcast.get('host', 'Unknown Host')}</p>
605
- <div class="podcast-delayed-2" style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden; border-radius: 8px;">
606
- <iframe
607
- src="https://www.youtube.com/embed/{youtube_id}?autoplay=1&rel=0&modestbranding=1&playsinline=1"
608
- style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border: none;"
609
- allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
610
- allowfullscreen>
611
- </iframe>
612
- </div>
613
- <p style="margin-top: 0.75rem; font-size: 0.85em; margin-bottom: 0;">
614
- <a href="{url}" target="_blank" style="color: #e9d8fd; text-decoration: none;">πŸ”— Open on YouTube</a>
615
- </p>
616
- </div>
617
- """
618
- print(f"βœ… Podcast iframe player created: {podcast.get('title', 'Unknown')} (ID: {youtube_id}) - shows after {delay_ms}ms")
 
 
 
 
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 - clears audio and music player"""
658
  radio_state["stop_flag"] = True
659
  radio_state["is_playing"] = False
660
- radio_state["planned_show"] = []
661
- radio_state["current_segment_index"] = 0
 
 
 
662
 
663
- # Return status, clear audio, clear music player, reset progress, reset now playing
 
 
664
  return (
665
- "⏹️ Radio stopped.",
666
  None, # Clear audio
667
  "", # Clear music player HTML
668
- "Stopped",
669
- "πŸ“» Ready to start"
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
- # Get host audio duration to calculate delay
853
- delay_ms = 0
854
- if audio_file and os.path.exists(audio_file):
855
- try:
856
- audio = AudioSegment.from_file(audio_file)
857
- duration_ms = len(audio)
858
- delay_ms = max(0, duration_ms - 3000) # Start 3 seconds before end
859
- print(f"⏱️ Voice request audio: {duration_ms}ms, YouTube delay: {delay_ms}ms")
860
- except Exception as e:
861
- print(f"⚠️ Could not get audio duration: {e}")
 
862
 
863
- # YouTube embed with delayed display
864
- music_player_html = f"""
865
- <style>
866
- @keyframes showVoiceYoutube {{
867
- from {{ opacity: 0; }}
868
- to {{ opacity: 1; }}
869
- }}
870
- .voice-youtube-delayed {{
871
- animation: showVoiceYoutube 0.5s ease-in forwards;
872
- animation-delay: {delay_ms}ms;
873
- opacity: 0;
874
- }}
875
- </style>
876
- <div style="padding: 1rem; background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%); border-radius: 12px; margin: 1rem 0; box-shadow: 0 4px 15px rgba(0,0,0,0.3);">
877
- <h4 style="margin: 0 0 0.75rem 0; color: #fff; font-size: 1.1em;">🎡 {track.get('title', 'Unknown')}</h4>
878
- <p style="margin: 0 0 0.75rem 0; color: #aaa; font-size: 0.9em;">by {track.get('artist', 'Unknown')}</p>
879
- <div class="voice-youtube-delayed" style="position: relative; width: 100%; padding-bottom: 56.25%; border-radius: 8px; overflow: hidden; background: #000;">
880
- <iframe
881
- style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border: none;"
882
- src="https://www.youtube.com/embed/{youtube_id}?autoplay=1&rel=0&modestbranding=1&playsinline=1"
883
- allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
884
- allowfullscreen>
885
- </iframe>
886
- </div>
887
- <p style="margin-top: 0.75rem; font-size: 0.85em; margin-bottom: 0;">
888
- <a href="{track['url']}" target="_blank" style="color: #ff4757; text-decoration: none;">πŸ”— Open on YouTube</a>
889
- </p>
890
- </div>
891
- """
892
- print(f"βœ… Voice request YouTube player created - shows after {delay_ms}ms")
893
- print(f"βœ… YouTube player created: {track.get('title', 'Unknown')} (ID: {youtube_id})")
 
 
 
 
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=7870,
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 = True) -> List[Dict[str, Any]]:
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 (recommended)
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
- search_limit = max(limit * 3, 15) if check_embed else max(limit, 10)
 
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
- # Check embeddability and take requested limit
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
- # Check if video is embeddable
 
 
 
 
 
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
- print(f" βœ“ Found embeddable: {track['title']} by {track['artist']}")
 
 
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=200 #200
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=200 #100
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 (no LLM yet)"""
268
- genres = preferences.get('favorite_genres', ['pop'])
269
- mood = preferences.get('mood', 'happy')
 
 
 
 
 
 
270
 
271
- # Use MCP server to get music
272
- tracks = self.music_server.search_free_music(
273
- genre=random.choice(genres),
274
- mood=mood,
275
- limit=1
276
- )
277
 
278
- track = tracks[0] if tracks else None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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=400 #400
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 = 800):
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=800 #800
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=200 #200
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=600 #600
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=200 #60 # Keep it short for fast response
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
- tracks = self.music_server.search_free_music(
659
- genre=selected_genre,
660
- mood=mood,
661
- limit=1
662
- )
663
- segment["track"] = tracks[0] if tracks else None
 
 
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
- rag_logger.info(f" πŸ“„ Retrieved #{len(results)}: user_id={node_user_id}, type={node_type}, item_type={item_type}, score={score:.4f if score else 'N/A'}, preview='{preview}...'")
545
- print(f" πŸ“„ [RAG] Retrieved #{len(results)}: {node_type} (user: {node_user_id}, score: {score:.4f if score else 'N/A'}) - {preview}...")
 
 
 
 
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
- rag_logger.warning(f" ⚠️ Retrieved #{len(results)} (parse error): user_id={node_user_id}, score={score}, text_preview='{node.text[:50]}...'")
565
- print(f" ⚠️ [RAG] Retrieved #{len(results)} (parse error, user: {node_user_id}, score: {score:.4f if score else 'N/A'})")
 
 
 
 
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