Surn's picture
0.2.18
7606f52
# 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)