import os import time import httpx import asyncio import edge_tts from datetime import datetime from typing import List, Dict, Any, Optional from mutagen.id3 import ID3, TIT2, TPE1, TALB, ID3NoHeaderError from app.config import settings from app.radio.weather import get_current_weather class RadioDJ: def __init__(self): self.ollama_url = f"{settings.OLLAMA_URL}/api/generate" self.model = settings.OLLAMA_MODEL self.default_voice = settings.RADIO_VOICE or "ru-RU-DmitryNeural" def get_time_of_day_info(self) -> Dict[str, str]: """Determine time of day greeting.""" hour = (datetime.utcnow().hour + 3) % 24 # Europe/Moscow UTC+3 approximation if 5 <= hour < 12: return {"greeting": "Доброе утро", "period": "утренний эфир"} elif 12 <= hour < 18: return {"greeting": "Добрый день", "period": "дневной эфир"} elif 18 <= hour < 23: return {"greeting": "Добрый вечер", "period": "вечерний эфир"} else: return {"greeting": "Доброй ночи", "period": "ночной эфир"} async def generate_speech_text( self, next_track: Optional[Dict[str, str]] = None, is_opening: bool = False ) -> str: """Generate charismatic short radio host remark via Ollama qwen2.5:1.5b.""" tod = self.get_time_of_day_info() weather = get_current_weather() weather_part = f"Погода: {weather}." if weather else "" track_part = f"Следующий трек: «{next_track['title']}» исполнителя {next_track['artist']}." if next_track else "" if is_opening: system_instruction = ( f"Ты харизматичный ведущий радиостанции «AI Радио». " f"Сейчас {tod['period']}, {tod['greeting']}. {weather_part} " f"Сделай открывающее приветствие радиослушателям (ровно 2 коротких предложения, до 25 слов) " f"и объяви первый трек: {track_part}. " f"Никаких приветствий от лица нейросети, никаких кавычек, ремарок в скобках или мета-текста. " f"Только чистая живая речь радиоведущего на русском языке." ) else: system_instruction = ( f"Ты классный ведущий радиостанции «AI Радио». " f"Сейчас в эфире {tod['period']}. " f"Сделай лаконичную подводку ведущего (1-2 предложения, до 20 слов) перед следующим треком: {track_part}. " f"Без кавычек, без смайлов, без скобок, только чистый текст ведущего на русском." ) # Call Ollama with keep_alive: 0 try: async with httpx.AsyncClient(timeout=25.0) as client: resp = await client.post( self.ollama_url, json={ "model": self.model, "prompt": system_instruction, "stream": False, "keep_alive": 0 } ) if resp.status_code == 200: text = resp.json().get("response", "").strip() # Clean up quotes, brackets, preamble text = text.replace('"', '').replace('«', '').replace('»', '').replace('*', '') lines = [l.strip() for l in text.split("\n") if l.strip()] if lines: cleaned = " ".join(lines) if len(cleaned) > 10: return cleaned except Exception as e: print(f"[RadioDJ] Ollama generation fallback due to: {e}") # Deterministic fallback if Ollama is unavailable if is_opening: w_str = f" За окном {weather}." if weather else "" t_str = f" Открываем эфир треком {next_track['artist']} — {next_track['title']}!" if next_track else " Начинаем наш музыкальный эфир!" return f"{tod['greeting']} в эфире AI Радио!{w_str}{t_str} Поехали!" else: if next_track: return f"Продолжаем поток на AI Радио. В эфире звучит {next_track['artist']} — {next_track['title']}." return "Вы слушаете AI Радио — только лучшая музыка для отличного настроения." def _apply_id3_tags(self, output_file: str, title: str): """Write ID3 tags so Navidrome categorizes host clips as AI Radio tracks.""" try: try: tags = ID3(output_file) except ID3NoHeaderError: tags = ID3() tags.add(TPE1(encoding=3, text=["AI Радио (DJ)"])) tags.add(TALB(encoding=3, text=["AI Радио Эфир"])) tags.add(TIT2(encoding=3, text=[title])) tags.save(output_file) except Exception as tag_err: print(f"[RadioDJ] Warning tagging audio: {tag_err}") async def synthesize_speech( self, text: str, output_file: str, title: str = "Ведущий в эфире", voice: Optional[str] = None ) -> bool: """Synthesize speech using edge-tts or rock-solid gTTS fallback and write ID3 tags for Navidrome.""" selected_voice = voice or self.default_voice # 1. Attempt edge-tts try: tts = edge_tts.Communicate(text, selected_voice) await tts.save(output_file) if os.path.exists(output_file) and os.path.getsize(output_file) > 500: self._apply_id3_tags(output_file, title) return True except Exception as e: print(f"[RadioDJ] edge-tts error ({e}), trying gTTS fallback...") # 2. Resilient fallback to gTTS try: from gtts import gTTS tts = gTTS(text=text, lang="ru") tts.save(output_file) if os.path.exists(output_file) and os.path.getsize(output_file) > 500: self._apply_id3_tags(output_file, title) return True except Exception as e: print(f"[RadioDJ] gTTS synthesis error: {e}") return False dj_host = RadioDJ()