import os import shutil from typing import List, Dict, Any, Optional from app.config import settings from app.wave.rotator import wave_rotator from app.radio.dj import dj_host class RadioManager: def __init__(self): self.radio_dir = os.path.join(settings.MUSIC_DIR, "AI Recommendations", "Радио") os.makedirs(self.radio_dir, exist_ok=True) def get_existing_speech_files(self) -> List[Dict[str, Any]]: """List currently generated DJ speech clips.""" if not os.path.exists(self.radio_dir): return [] files = [] for f in sorted(os.listdir(self.radio_dir)): if f.lower().endswith(".mp3"): fpath = os.path.join(self.radio_dir, f) stat = os.stat(fpath) files.append({ "filename": f, "path": fpath, "size_kb": round(stat.st_size / 1024, 1), "mtime": stat.st_mtime }) return files async def generate_radio_broadcast( self, voice: Optional[str] = None, max_segments: int = 5 ) -> Dict[str, Any]: """Generate AI DJ speech intros and build the 'AI Радио.m3u8' interleaved playlist.""" wave_tracks = wave_rotator.get_current_buffer() if not wave_tracks: # If buffer is empty, refill it first print("[RadioManager] Wave buffer empty. Sourcing wave tracks first...") await wave_rotator.fill_buffer(mood="auto") wave_tracks = wave_rotator.get_current_buffer() if not wave_tracks: return {"status": "error", "message": "Нет доступных треков в буфере Волны"} # Clean old DJ voice tracks for old_file in os.listdir(self.radio_dir): if old_file.endswith(".mp3"): try: os.remove(os.path.join(self.radio_dir, old_file)) except Exception as e: print(f"[RadioManager] Warning removing old radio file: {e}") # Plan DJ speech checkpoints (e.g. before track 0, before track 3, before track 7, etc.) total_tracks = len(wave_tracks) checkpoints = [0] step = max(2, settings.RADIO_INTRO_INTERVAL) curr = step while curr < total_tracks and len(checkpoints) < max_segments: checkpoints.append(curr) curr += step generated_segments = [] speech_map: Dict[int, str] = {} for idx, track_idx in enumerate(checkpoints): next_t = wave_tracks[track_idx] is_opening = (idx == 0) # 1. Generate DJ speech text speech_text = await dj_host.generate_speech_text( next_track={"artist": next_t["artist"], "title": next_t["title"]}, is_opening=is_opening ) # 2. Synthesize audio file segment_filename = f"{idx+1:02d}_dj_intro.mp3" segment_path = os.path.join(self.radio_dir, segment_filename) title = f"AI DJ: Приветствие и погода" if is_opening else f"AI DJ: Подводка к «{next_t['title']}»" success = await dj_host.synthesize_speech( text=speech_text, output_file=segment_path, title=title, voice=voice ) if success: speech_map[track_idx] = segment_filename generated_segments.append({ "segment": segment_filename, "text": speech_text, "before_track": f"{next_t['artist']} - {next_t['title']}" }) print(f"[RadioManager] 🎙️ Created DJ clip {segment_filename}: \"{speech_text}\"") # 3. Assemble interleaved M3U8 playlist playlist_lines = ["#EXTM3U\n"] for i, t in enumerate(wave_tracks): # Insert DJ voice if checkpoint if i in speech_map: dj_file = speech_map[i] playlist_lines.append(f"#EXTINF:-1,AI DJ - Эфирная подводка\n") playlist_lines.append(f"AI Recommendations/Радио/{dj_file}\n") # Music track playlist_lines.append(f"#EXTINF:-1,{t['artist']} - {t['title']}\n") playlist_lines.append(f"AI Recommendations/Моя Волна/{t['filename']}\n") # Write to /music/AI Радио.m3u8 m3u_root_path = os.path.join(settings.MUSIC_DIR, "AI Радио.m3u8") with open(m3u_root_path, "w", encoding="utf-8") as f: f.writelines(playlist_lines) # Write to /music/playlists/AI Радио.m3u8 playlists_dir = os.path.join(settings.MUSIC_DIR, "playlists") os.makedirs(playlists_dir, exist_ok=True) m3u_playlists_path = os.path.join(playlists_dir, "AI Радио.m3u8") with open(m3u_playlists_path, "w", encoding="utf-8") as f: f.writelines(playlist_lines) # Trigger background library scan in Navidrome try: from app.core.navidrome import trigger_navidrome_scan await trigger_navidrome_scan() except Exception as e: print(f"[RadioManager] Warning triggering Navidrome scan: {e}") return { "status": "ok", "playlist_name": "AI Радио", "m3u_path": m3u_root_path, "total_music_tracks": len(wave_tracks), "dj_segments_count": len(generated_segments), "segments": generated_segments } def get_radio_status(self) -> Dict[str, Any]: """Get summary of current AI Radio state.""" speech_files = self.get_existing_speech_files() m3u_path = os.path.join(settings.MUSIC_DIR, "AI Радио.m3u8") playlist_ready = os.path.exists(m3u_path) return { "enabled": settings.RADIO_ENABLED, "playlist_ready": playlist_ready, "speech_clips_count": len(speech_files), "speech_clips": speech_files, "default_voice": settings.RADIO_VOICE, "available_voices": [ {"id": "ru-RU-DmitryNeural", "name": "Дмитрий (Харизматичный DJ)", "gender": "Мужской"}, {"id": "ru-RU-SvetlanaNeural", "name": "Светлана (Мягкий вечерний голос)", "gender": "Женский"} ] } radio_manager = RadioManager()