# file: battlewords/sounds.py import os import tempfile import base64 import requests import time from io import BytesIO from pathlib import Path # Load environment variables from .env file def _load_env(): """Load .env file from project root""" env_path = Path(__file__).parent.parent / ".env" if env_path.exists(): with open(env_path) as f: for line in f: line = line.strip() if line and not line.startswith("#") and "=" in line: key, value = line.split("=", 1) os.environ.setdefault(key.strip(), value.strip()) _load_env() # Predefined prompts for sound effects: key: {"prompt": text, "duration": seconds} EFFECT_PROMPTS = { "correct_guess": {"prompt": "A short, sharp ding sound for a correct guess", "duration": 2}, "incorrect_guess": {"prompt": "A low buzz sound for an incorrect guess", "duration": 2}, "miss": {"prompt": "A soft thud sound for a miss", "duration": 1}, "hit": {"prompt": "A bright chime sound for a hit", "duration": 1}, "congratulations": {"prompt": "A triumphant fanfare sound for congratulations", "duration": 3} } _sound_cache = {} # Hugging Face Inference API configuration # Loaded from .env file or environment variable: HF_API_TOKEN HF_API_TOKEN = os.environ.get("HF_API_TOKEN", None) if HF_API_TOKEN: print(f"Using HF_API_TOKEN: {HF_API_TOKEN[:10]}...") HF_API_URL = "https://api-inference.huggingface.co/models/facebook/audiogen-medium" def generate_sound_effect(effect: str, save_to_assets: bool = False, use_api: str = "huggingface") -> str: """ Generate a sound effect using external API based on the effect string. Returns the path to the generated audio file. Args: effect: Name of the effect (must be in EFFECT_PROMPTS) save_to_assets: If True, save to battlewords/assets/audio/effects/ instead of temp directory use_api: API to use - "huggingface" (default) or "replicate" """ if effect not in EFFECT_PROMPTS: raise ValueError(f"Unknown effect: {effect}. Available effects: {list(EFFECT_PROMPTS.keys())}") # Check cache first (only for temp files) if effect in _sound_cache and not save_to_assets: if os.path.exists(_sound_cache[effect]): return _sound_cache[effect] effect_config = EFFECT_PROMPTS[effect] prompt = effect_config["prompt"] duration = effect_config["duration"] print(f"Generating sound effect: {effect}") print(f" Prompt: {prompt}") print(f" Duration: {duration}s") print(f" Using API: {use_api}") audio_bytes = None if use_api == "huggingface": audio_bytes = _generate_via_huggingface(prompt, duration) else: raise ValueError(f"Unknown API: {use_api}") if audio_bytes is None: raise RuntimeError(f"Failed to generate sound effect: {effect}") # Determine save location if save_to_assets: # Save to effects assets directory (preferred by audio.py) assets_dir = os.path.join(os.path.dirname(__file__), "assets", "audio", "effects") os.makedirs(assets_dir, exist_ok=True) filename = f"{effect}.wav" path = os.path.join(assets_dir, filename) with open(path, "wb") as f: f.write(audio_bytes) print(f" Saved to: {path}") else: # Save to temporary file with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as tmpfile: tmpfile.write(audio_bytes) path = tmpfile.name print(f" Saved to: {path}") # Cache the path _sound_cache[effect] = path return path def _generate_via_huggingface(prompt: str, duration: float, max_retries: int = 3) -> bytes: """ Generate audio using Hugging Face Inference API. Uses facebook/audiogen-medium model for sound effects. """ headers = {} if HF_API_TOKEN: headers["Authorization"] = f"Bearer {HF_API_TOKEN}" payload = { "inputs": prompt, "parameters": { "duration": duration } } for attempt in range(max_retries): try: print(f" Calling Hugging Face API (attempt {attempt + 1}/{max_retries})...") response = requests.post(HF_API_URL, headers=headers, json=payload, timeout=60) if response.status_code == 503: # Model is loading, wait and retry print(f" Model loading, waiting 10 seconds...") time.sleep(10) continue if response.status_code == 200: print(f" Success! Received {len(response.content)} bytes") return response.content else: print(f" Error {response.status_code}: {response.text}") if attempt < max_retries - 1: time.sleep(5) continue else: raise RuntimeError(f"API request failed: {response.status_code} - {response.text}") except requests.exceptions.Timeout: print(f" Request timed out") if attempt < max_retries - 1: time.sleep(5) continue else: raise RuntimeError("API request timed out after multiple attempts") except Exception as e: print(f" Error: {e}") if attempt < max_retries - 1: time.sleep(5) continue else: raise return None def get_sound_effect_path(effect: str) -> str: """ Get the path to a sound effect, generating it if necessary. """ return generate_sound_effect(effect) def get_sound_effect_data_url(effect: str) -> str: """ Get a data URL for the sound effect, suitable for embedding in HTML. """ path = generate_sound_effect(effect) with open(path, "rb") as f: data = f.read() encoded = base64.b64encode(data).decode() return f"data:audio/wav;base64,{encoded}" def generate_all_effects(save_to_assets: bool = True): """ Generate all sound effects defined in EFFECT_PROMPTS. Args: save_to_assets: If True, save to battlewords/assets/audio/effects/ directory """ print(f"\nGenerating {len(EFFECT_PROMPTS)} sound effects...") print("=" * 60) for effect_name in EFFECT_PROMPTS.keys(): try: generate_sound_effect(effect_name, save_to_assets=save_to_assets) print() except Exception as e: print(f"ERROR generating {effect_name}: {e}") print() print("=" * 60) print("Sound effect generation complete!") if __name__ == "__main__": # Generate all sound effects when run as a script generate_all_effects(save_to_assets=True)