#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ Streamlit web app for chorus detection in audio files. """ import os import sys import logging import base64 import tempfile import warnings import io from typing import Optional, Tuple, List import matplotlib.pyplot as plt import streamlit as st import tensorflow as tf import librosa import soundfile as sf import numpy as np from pydub import AudioSegment # Configure logging logger = logging.getLogger("streamlit-app") # Suppress TensorFlow and other warnings os.environ['TF_CPP_MIN_LOG_LEVEL'] = '2' warnings.filterwarnings("ignore") tf.get_logger().setLevel('ERROR') # Import components try: from download_model import ensure_model_exists from chorus_detection.audio.data_processing import process_audio from chorus_detection.audio.processor import extract_audio from chorus_detection.models.crnn import load_CRNN_model, make_predictions from chorus_detection.utils.cli import is_youtube_url from chorus_detection.utils.logging import logger logger.info("Successfully imported chorus_detection modules") except ImportError as e: logger.error(f"Error importing modules: {e}") raise # Define model path MODEL_PATH = os.path.join(os.getcwd(), "models", "CRNN", "best_model_V3.h5") if not os.path.exists(MODEL_PATH): MODEL_PATH = ensure_model_exists() # UI theme colors THEME_COLORS = { 'background': '#121212', 'card_bg': '#181818', 'primary': '#1DB954', 'secondary': '#1ED760', 'text': '#FFFFFF', 'subtext': '#B3B3B3', 'highlight': '#1DB954', 'border': '#333333', } def get_binary_file_downloader_html(bin_file: str, file_label: str = 'File') -> str: """Generate HTML for file download link.""" with open(bin_file, 'rb') as f: data = f.read() b64 = base64.b64encode(data).decode() return f'{file_label}' def set_custom_theme() -> None: """Apply custom Spotify-inspired theme to Streamlit UI.""" custom_theme = f""" """ st.markdown(custom_theme, unsafe_allow_html=True) def process_youtube(url: str) -> Tuple[Optional[str], Optional[str]]: """Process a YouTube URL and extract audio.""" try: with st.spinner('Downloading audio from YouTube...'): audio_path, video_name = extract_audio(url) return audio_path, video_name except Exception as e: st.error(f"Error processing YouTube URL: {e}") logger.error(f"Error processing YouTube URL: {e}", exc_info=True) return None, None def process_uploaded_file(uploaded_file) -> Tuple[Optional[str], Optional[str]]: """Process an uploaded audio file.""" try: with st.spinner('Processing uploaded file...'): temp_dir = tempfile.mkdtemp() file_name = uploaded_file.name temp_path = os.path.join(temp_dir, file_name) with open(temp_path, 'wb') as f: f.write(uploaded_file.getbuffer()) return temp_path, file_name.split('.')[0] except Exception as e: st.error(f"Error processing uploaded file: {e}") logger.error(f"Error processing uploaded file: {e}", exc_info=True) return None, None def extract_chorus_segments(y: np.ndarray, sr: int, smoothed_predictions: np.ndarray, meter_grid_times: np.ndarray) -> List[Tuple[float, float, np.ndarray]]: """Extract chorus segments from predictions.""" threshold = 0.5 chorus_mask = smoothed_predictions > threshold segments = [] current_segment = None for i, is_chorus in enumerate(chorus_mask): time = meter_grid_times[i] if is_chorus and current_segment is None: current_segment = (time, None, None) elif not is_chorus and current_segment is not None: start_time = current_segment[0] current_segment = (start_time, time, None) segments.append(current_segment) current_segment = None # Handle the case where the last segment extends to the end of the song if current_segment is not None: start_time = current_segment[0] segments.append((start_time, meter_grid_times[-1], None)) # Extract the actual audio for each segment segments_with_audio = [] for start_time, end_time, _ in segments: start_idx = int(start_time * sr) end_idx = int(end_time * sr) segment_audio = y[start_idx:end_idx] segments_with_audio.append((start_time, end_time, segment_audio)) return segments_with_audio def create_chorus_compilation(segments: List[Tuple[float, float, np.ndarray]], sr: int, fade_duration: float = 0.3) -> Tuple[np.ndarray, str]: """Create a compilation of chorus segments.""" if not segments: return np.array([]), "No chorus segments found" fade_samples = int(fade_duration * sr) processed_segments = [] segment_descriptions = [] for i, (start_time, end_time, audio) in enumerate(segments): segment_length = len(audio) if segment_length <= 2 * fade_samples: continue fade_in = np.linspace(0, 1, fade_samples) fade_out = np.linspace(1, 0, fade_samples) audio_faded = audio.copy() audio_faded[:fade_samples] *= fade_in audio_faded[-fade_samples:] *= fade_out processed_segments.append(audio_faded) start_fmt = format_time(start_time) end_fmt = format_time(end_time) segment_descriptions.append(f"Chorus {i+1}: {start_fmt} - {end_fmt}") if not processed_segments: return np.array([]), "No chorus segments long enough for compilation" compilation = np.concatenate(processed_segments) description = "\n".join(segment_descriptions) return compilation, description def save_audio_for_streamlit(audio_data: np.ndarray, sr: int, file_format: str = 'mp3') -> bytes: """Save audio data to a format suitable for Streamlit audio playback.""" with io.BytesIO() as buffer: sf.write(buffer, audio_data, sr, format=file_format) buffer.seek(0) return buffer.read() def format_time(seconds: float) -> str: """Format seconds as MM:SS.""" minutes = int(seconds // 60) seconds = int(seconds % 60) return f"{minutes:02d}:{seconds:02d}" def main() -> None: """Main function for the Streamlit app.""" # Set page config st.set_page_config( page_title="Chorus Detection", page_icon="🎵", layout="wide", initial_sidebar_state="collapsed", ) # Apply custom theme set_custom_theme() # App title and description st.title("Chorus Detection") st.markdown("""