#!/usr/bin/env python3 """ VYNL Complete - HuggingFace Edition Full-featured music production suite with AI generation and voice cloning. Copyright (c) 2024-2026 Robert T. Lackey. All rights reserved. Stone and Lantern Music Group """ import os # ZeroGPU support - MUST be imported before other heavy imports # This needs to be at the very top for HuggingFace detection IS_HF_SPACE = os.environ.get('SPACE_ID') is not None if IS_HF_SPACE: import spaces HAS_ZEROGPU = True print("ZeroGPU available - running on HuggingFace Spaces") else: HAS_ZEROGPU = False print("Running locally - ZeroGPU not available") # Dummy decorator for local use class spaces: @staticmethod def GPU(duration=60): def decorator(func): return func return decorator import gradio as gr import json import tempfile import shutil from pathlib import Path from datetime import datetime import warnings warnings.filterwarnings('ignore') # ============================================================================ # ZEROGPU QUOTA ERROR HANDLING # ============================================================================ QUOTA_ERROR_MSG = """⚠️ ZeroGPU Quota Exceeded You've run out of free GPU quota for today. To get more GPU time: 1. Sign up for free: https://huggingface.co/join 2. Or log in: https://huggingface.co/login Logged-in users get significantly more GPU quota! """ def is_quota_error(error_msg: str) -> bool: """Check if an error is a ZeroGPU quota error""" quota_keywords = ['quota', 'ZeroGPU', 'daily', 'limit', 'exceeded'] error_lower = str(error_msg).lower() return any(kw.lower() in error_lower for kw in quota_keywords) def handle_gpu_error(e: Exception) -> str: """Return user-friendly message for GPU errors""" error_msg = str(e) if is_quota_error(error_msg): return QUOTA_ERROR_MSG return f"GPU Error: {error_msg}" # ============================================================================ # MODULE IMPORTS # ============================================================================ # Token system from token_system import ( user_manager, check_can_process, deduct_token, get_status_display, DEMO_MAX_DURATION, VALID_LICENSES ) # Mastering from mastering import master_audio, format_analysis, analyze_audio # Catalog system try: from vynl_catalog import ( SongCatalog, VoiceCatalog, GeneratedCatalog, get_all_audio_for_user ) HAS_CATALOG = True except ImportError: HAS_CATALOG = False print("Catalog module not available") # Chord detection module try: from modules.chords import extract_chords, format_chord_chart HAS_CHORD_DETECTION = True except ImportError: HAS_CHORD_DETECTION = False print("Chord detection module not available") def format_chord_chart(*args, **kwargs): return "" # Stem separation module (for 2-pass separation) try: from modules.stems import separate_stems as module_separate_stems, list_stems HAS_STEM_MODULE = True except ImportError: HAS_STEM_MODULE = False print("Stem separation module not available") # Reaper project generator try: from modules.reaper import create_reaper_project HAS_REAPER_MODULE = True except ImportError: HAS_REAPER_MODULE = False print("Reaper module not available") import zipfile # AI Generator (optional) HAS_AUDIOCRAFT = False try: from vynl_generator import ( generate_song, KEYS, TIME_SIGNATURES, GENRES, MOODS, INSTRUMENTS, HAS_AUDIOCRAFT, build_prompt ) except ImportError as e: print(f"AI Generator not available: {e}") KEYS = ["C major", "C minor", "D major", "D minor", "E major", "E minor", "F major", "F minor", "G major", "G minor", "A major", "A minor", "B major", "B minor"] TIME_SIGNATURES = ["4/4", "3/4", "6/8", "2/4", "5/4", "7/8"] GENRES = ["Pop", "Rock", "Jazz", "Blues", "Electronic", "Classical", "Hip Hop", "R&B", "Country", "Folk"] MOODS = ["Happy", "Sad", "Energetic", "Calm", "Dark", "Uplifting", "Melancholic"] INSTRUMENTS = ["Piano", "Guitar", "Drums", "Bass", "Strings", "Synth", "Brass", "Woodwinds"] def generate_song(*args, **kwargs): return None, "AI Generator not available - install audiocraft: pip install audiocraft xformers" # RVC Voice Cloning (optional) HAS_RVC = False RVC_INFO = "RVC not loaded" PRESET_VOICES = { "male_tenor": {"name": "Male Tenor", "pitch_shift": 0}, "male_bass": {"name": "Male Bass", "pitch_shift": -5}, "female_alto": {"name": "Female Alto", "pitch_shift": 12}, "female_soprano": {"name": "Female Soprano", "pitch_shift": 15}, } try: from vynl_rvc import ( clone_voice, train_voice_model, get_model_registry, get_voice_dataset, PRESET_VOICES as RVC_PRESETS, check_rvc_installation ) HAS_RVC, RVC_INFO = check_rvc_installation() if RVC_PRESETS: PRESET_VOICES = RVC_PRESETS except ImportError as e: print(f"RVC not available: {e}") def clone_voice(*args, **kwargs): return None, "RVC not available - install with: pip install rvc-python" def train_voice_model(*args, **kwargs): return None, "RVC not available" # Audio processing try: import librosa import numpy as np HAS_LIBROSA = True except ImportError: HAS_LIBROSA = False print("Librosa not available") try: import yt_dlp HAS_YTDLP = True except ImportError: HAS_YTDLP = False print("yt-dlp not available") # ============================================================================ # CONFIGURATION # ============================================================================ VERSION = "2.2" OUTPUT_DIR = Path(tempfile.gettempdir()) / 'vynl_output' OUTPUT_DIR.mkdir(parents=True, exist_ok=True) # ============================================================================ # CSS THEME # ============================================================================ CSS = """ /* VYNL HuggingFace - High Contrast Dark Theme */ :root { --bg-dark: #121212; --bg-panel: #1E1E1E; --bg-input: #2A2A2A; --border: #404040; --text: #FFFFFF; --text-dim: #B0B0B0; --accent: #FF6B4A; --accent-cyan: #00D4FF; --accent-green: #7CFF00; } /* Global */ .gradio-container, .gradio-container * { color: var(--text) !important; } body, .gradio-container { background: var(--bg-dark) !important; max-width: 1400px !important; } /* Header */ .vynl-header { text-align: center; padding: 24px; background: linear-gradient(180deg, rgba(255,107,74,0.15) 0%, transparent 100%); border-bottom: 3px solid; border-image: linear-gradient(90deg, var(--accent-cyan), var(--accent), var(--accent-green)) 1; margin-bottom: 16px; } .vynl-header h1 { font-size: 3rem; font-weight: 900; letter-spacing: 0.2em; background: linear-gradient(90deg, var(--accent-cyan), var(--accent), var(--accent-green)); -webkit-background-clip: text; -webkit-text-fill-color: transparent; margin: 0; } .vynl-header p { color: var(--text-dim) !important; margin: 8px 0 0; } /* Status bar */ .status-bar { background: rgba(0,212,255,0.15); border: 1px solid rgba(0,212,255,0.4); border-radius: 25px; padding: 10px 24px; margin: 12px auto; max-width: 700px; text-align: center; color: var(--text) !important; } /* Tabs */ .tabs { border: none !important; } .tab-nav { background: transparent !important; gap: 8px !important; justify-content: center !important; flex-wrap: wrap !important; } .tab-nav button { background: var(--bg-panel) !important; border: 1px solid var(--border) !important; color: var(--text) !important; padding: 12px 24px !important; border-radius: 10px !important; font-weight: 600 !important; } .tab-nav button:hover { border-color: var(--accent) !important; background: #252525 !important; } .tab-nav button.selected { background: var(--accent) !important; border-color: var(--accent) !important; } /* Inputs */ input, textarea, select { background: var(--bg-input) !important; border: 1px solid var(--border) !important; color: var(--text) !important; border-radius: 8px !important; } input::placeholder, textarea::placeholder { color: #707070 !important; } input:focus, textarea:focus { border-color: var(--accent) !important; } /* Labels */ label { color: var(--text) !important; font-weight: 500 !important; } .markdown, .gr-markdown, p, h1, h2, h3, h4, li, span { color: var(--text) !important; } /* Panels */ .gr-box, .gr-panel, .gr-form, .gr-block { background: var(--bg-panel) !important; border: 1px solid var(--border) !important; border-radius: 10px !important; } /* Buttons */ button, .gr-button { color: var(--text) !important; } .gr-button-primary { background: linear-gradient(135deg, var(--accent), #FF8C42) !important; border: none !important; } .gr-button-secondary { background: var(--bg-panel) !important; border: 1px solid var(--border) !important; } /* Accordion */ .gr-accordion { background: var(--bg-panel) !important; border: 1px solid var(--border) !important; } .gr-accordion summary { color: var(--text) !important; } /* Dropdowns */ .gr-dropdown, [data-testid="dropdown"] { background: var(--bg-input) !important; } .gr-dropdown span, .gr-dropdown label { color: var(--text) !important; } .gr-dropdown ul, .gr-dropdown li { background: var(--bg-input) !important; color: var(--text) !important; } /* Slider */ .gr-slider span, .gr-slider label { color: var(--text) !important; } /* Audio */ .gr-audio { background: var(--bg-panel) !important; } /* Tables */ table, th, td { color: var(--text) !important; background: var(--bg-panel) !important; } th { background: var(--bg-input) !important; } /* Footer */ footer { display: none !important; } .vynl-footer { text-align: center; padding: 20px; color: var(--text-dim) !important; border-top: 1px solid var(--border); margin-top: 24px; } .vynl-footer p { color: var(--text-dim) !important; } .vynl-footer strong { color: var(--text) !important; } """ # ============================================================================ # AUDIO PROCESSING FUNCTIONS # ============================================================================ def download_youtube(url: str) -> tuple: """Download audio from YouTube using CLI for better cookie support""" import subprocess import shutil yt_dlp_path = shutil.which('yt-dlp') if not yt_dlp_path: return None, "yt-dlp not installed" try: output_dir = OUTPUT_DIR / f"yt_{datetime.now().strftime('%H%M%S')}" output_dir.mkdir(parents=True, exist_ok=True) audio_path = output_dir / "audio.wav" # Check for cookies.txt file cookies_file = Path(__file__).parent / "cookies.txt" def build_cmd(with_cookies=None): cmd = [yt_dlp_path, '--socket-timeout', '60', '--retries', '5', '--no-warnings'] if with_cookies == 'file' and cookies_file.exists(): cmd.extend(['--cookies', str(cookies_file)]) elif with_cookies and not IS_HF_SPACE: cmd.extend(['--cookies-from-browser', with_cookies]) return cmd # Try different cookie sources (skip browser cookies on HF Space) cookie_sources = [] if cookies_file.exists(): cookie_sources.append('file') if not IS_HF_SPACE: cookie_sources.extend(['chrome', 'edge', 'firefox', 'brave']) cookie_sources.append(None) # Try without cookies last title = 'Unknown' download_success = False last_error = "" for cookie_source in cookie_sources: source_name = "cookies.txt" if cookie_source == 'file' else (cookie_source or 'no cookies') print(f"Trying YouTube download with {source_name}...") base_cmd = build_cmd(cookie_source) # Get title title_cmd = base_cmd + ['--print', 'title', '--no-download', url] result = subprocess.run(title_cmd, capture_output=True, text=True, timeout=30) if result.returncode == 0 and result.stdout.strip(): title = result.stdout.strip() # Download audio audio_cmd = base_cmd + [ '-f', 'bestaudio/best', '-x', '--audio-format', 'wav', '-o', str(audio_path).replace('.wav', '.%(ext)s'), url ] print(f"Downloading: {title}") result = subprocess.run(audio_cmd, capture_output=True, text=True, timeout=300) if result.returncode == 0: download_success = True break last_error = result.stderr if result.stderr else "Unknown error" if 'Sign in' not in last_error and 'bot' not in last_error.lower(): break if not download_success: if IS_HF_SPACE: return None, "YouTube requires authentication on HuggingFace. Please upload the audio file directly." return None, f"{last_error}\n\nTry uploading the audio file directly instead." # Find the audio file if not audio_path.exists(): for f in output_dir.glob('audio.*'): if f.suffix == '.wav': audio_path = f break return str(audio_path), title except subprocess.TimeoutExpired: return None, "Download timed out. Try a shorter video." except Exception as e: return None, str(e) @spaces.GPU(duration=120) def _separate_stems_gpu(audio_path: str, progress=None, two_stem: bool = False) -> dict: """Separate audio into stems using Demucs (GPU accelerated) - internal""" try: import subprocess mode = "2stem" if two_stem else "6stem" stems_dir = OUTPUT_DIR / f"stems_{mode}_{datetime.now().strftime('%H%M%S')}" stems_dir.mkdir(exist_ok=True) if progress: if two_stem: progress(0.3, "Running Demucs 2-stem separation (vocals/instrumental)...") else: progress(0.3, "Running Demucs 6-stem separation (drums, bass, guitar, keys, other, vocals)...") # Build command based on mode if two_stem: cmd = ['python', '-m', 'demucs', '--two-stems=vocals', '-o', str(stems_dir), '--mp3', '--mp3-bitrate=320', audio_path] else: cmd = ['python', '-m', 'demucs', '-n', 'htdemucs_6s', '-o', str(stems_dir), '--mp3', '--mp3-bitrate=320', audio_path] subprocess.run(cmd, capture_output=True, check=True) # Find output stems model_dir = list(stems_dir.glob('*'))[0] song_dir = list(model_dir.glob('*'))[0] stems = {} for stem_file in song_dir.glob('*.mp3'): stem_name = stem_file.stem stems[stem_name] = str(stem_file) # Also check for wav files for stem_file in song_dir.glob('*.wav'): stem_name = stem_file.stem if stem_name not in stems: stems[stem_name] = str(stem_file) return {'stems': stems, 'stems_dir': str(song_dir)} except Exception as e: return {'error': str(e)} def separate_stems(audio_path: str, progress=None, two_stem: bool = False) -> dict: """Separate stems - wrapper with quota error handling""" try: return _separate_stems_gpu(audio_path, progress, two_stem) except Exception as e: return {'error': handle_gpu_error(e)} def detect_key_and_tempo(audio_path: str) -> dict: """Detect musical key and tempo""" if not HAS_LIBROSA: return {'key': 'Unknown', 'bpm': 120} try: y, sr = librosa.load(audio_path, duration=60) # Tempo tempo, _ = librosa.beat.beat_track(y=y, sr=sr) bpm = float(tempo) if hasattr(tempo, '__float__') else float(tempo[0]) # Key detection via chroma chroma = librosa.feature.chroma_cqt(y=y, sr=sr) chroma_avg = np.mean(chroma, axis=1) key_idx = np.argmax(chroma_avg) key_names = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B'] key = key_names[key_idx] # Major/minor estimation major_profile = np.array([6.35, 2.23, 3.48, 2.33, 4.38, 4.09, 2.52, 5.19, 2.39, 3.66, 2.29, 2.88]) minor_profile = np.array([6.33, 2.68, 3.52, 5.38, 2.60, 3.53, 2.54, 4.75, 3.98, 2.69, 3.34, 3.17]) major_corr = np.corrcoef(chroma_avg, np.roll(major_profile, key_idx))[0, 1] minor_corr = np.corrcoef(chroma_avg, np.roll(minor_profile, key_idx))[0, 1] mode = "major" if major_corr > minor_corr else "minor" duration = librosa.get_duration(y=y, sr=sr) return { 'key': f"{key} {mode}", 'bpm': round(bpm), 'duration': duration } except Exception as e: return {'key': 'Unknown', 'bpm': 120, 'error': str(e)} # ============================================================================ # MAIN PROCESSING # ============================================================================ def process_song(audio, yt_url, name, lyrics, do_stems, do_chords, do_daw, user_email, progress=gr.Progress()): """Process a song - stems, chords, analysis""" progress(0.1, "Starting processing...") # Get audio file audio_path = None song_name = name if audio: audio_path = audio if not song_name: song_name = Path(audio).stem elif yt_url: progress(0.15, "Downloading from YouTube...") audio_path, title = download_youtube(yt_url) if not audio_path: return f"Download failed: {title}", None, get_status_display(user_email) if not song_name: song_name = title else: return "Please provide audio file or YouTube URL", None, get_status_display(user_email) log = [f"Processing: {song_name}"] # Analyze progress(0.2, "Analyzing audio...") analysis = detect_key_and_tempo(audio_path) log.append(f"Key: {analysis.get('key', 'Unknown')} | BPM: {analysis.get('bpm', 120)}") # Stems stems = {} if do_stems: progress(0.3, "Separating stems...") try: stems = separate_stems(audio_path, progress) if 'error' in stems: error_msg = stems['error'] if is_quota_error(error_msg): log.append(QUOTA_ERROR_MSG) else: log.append(f"Stem separation error: {error_msg}") else: log.append(f"Stems: {', '.join(stems.keys())}") except Exception as e: log.append(handle_gpu_error(e)) # Chords chords_text = None chords_list = [] if do_chords: progress(0.7, "Detecting chords...") if HAS_CHORD_DETECTION: try: chords_list = extract_chords(audio_path, min_duration=1.0) if chords_list: # Format chord chart - with lyrics alignment if lyrics provided duration = analysis.get('duration', 180) chords_text = format_chord_chart( chords=chords_list, lyrics=lyrics, duration=duration, key=analysis.get('key', 'C'), bpm=analysis.get('bpm', 120), song_name=song_name ) if lyrics: log.append(f"Chord chart: {len(chords_list)} chords aligned to lyrics") else: log.append(f"Chord chart: {len(chords_list)} changes detected (add lyrics for aligned chart)") else: chords_text = f"# {song_name}\nKey: {analysis.get('key', 'C')}\nBPM: {analysis.get('bpm', 120)}\n\n(No chord changes detected)" log.append("Chord detection: no changes found") except Exception as e: chords_text = f"# {song_name}\nChord detection error: {str(e)}" log.append(f"Chord detection error: {e}") else: chords_text = f"# {song_name}\nKey: {analysis.get('key', 'C')}\nBPM: {analysis.get('bpm', 120)}\n\n(Chord detection not available)" log.append("Chord detection not available") # Save to catalog if HAS_CATALOG: progress(0.9, "Saving to catalog...") catalog = SongCatalog(user_email or "demo") song_id = catalog.add_song( name=song_name, original_path=audio_path, stems=stems if stems and 'error' not in stems else None, chords=chords_text, lyrics=lyrics, key=analysis.get('key'), bpm=analysis.get('bpm'), duration=analysis.get('duration'), source='youtube' if yt_url else 'upload' ) log.append(f"Saved to catalog: {song_id}") progress(1.0, "Complete!") return "\n".join(log), audio_path, get_status_display(user_email) # ============================================================================ # SONG PACKAGE CREATION # ============================================================================ def create_song_package(audio, yt_url, name, lyrics, user_email, progress=gr.Progress()): """ Create a complete song package with: - Pass 1 stems (vocals, instrumental) - Pass 2 stems (drums, bass, guitar, keys, other, vocals) - Mastered original (Warm preset) - Chord chart - Reaper project file All bundled in a zip named after the song """ progress(0.05, "Starting song package creation...") # Get audio file audio_path = None song_name = name if audio: audio_path = audio if not song_name: song_name = Path(audio).stem elif yt_url: progress(0.08, "Downloading from YouTube...") audio_path, title = download_youtube(yt_url) if not audio_path: return f"Download failed: {title}", None, get_status_display(user_email) if not song_name: song_name = title else: return "Please provide audio file or YouTube URL", None, get_status_display(user_email) # Clean song name for filesystem safe_name = "".join(c for c in song_name if c.isalnum() or c in (' ', '-', '_')).strip() safe_name = safe_name.replace(' ', '_') log = [f"Creating package for: {song_name}"] # Create package directory package_dir = OUTPUT_DIR / f"package_{safe_name}_{datetime.now().strftime('%H%M%S')}" package_dir.mkdir(parents=True, exist_ok=True) # === ANALYZE AUDIO === progress(0.10, "Analyzing audio...") analysis = detect_key_and_tempo(audio_path) tempo = analysis.get('bpm', 120) key = analysis.get('key', 'C') log.append(f"Key: {key} | BPM: {tempo}") # === PASS 1: 2-STEM SEPARATION (vocals/instrumental) - GPU ACCELERATED === stems_pass1 = {} stems_pass1_dir = None progress(0.15, "Pass 1: Separating vocals/instrumental (GPU)...") try: result = separate_stems(audio_path, progress, two_stem=True) if 'error' in result: error_msg = result['error'] if is_quota_error(str(error_msg)): log.append(QUOTA_ERROR_MSG) else: log.append(f"Pass 1 error: {error_msg}") else: stems_pass1 = result.get('stems', {}) stems_pass1_dir = result.get('stems_dir') log.append(f"Pass 1 stems: {', '.join(stems_pass1.keys())}") except Exception as e: log.append(f"Pass 1 error: {handle_gpu_error(e)}") # === PASS 2: 6-STEM SEPARATION (detailed) - GPU ACCELERATED === stems_pass2 = {} stems_pass2_dir = None progress(0.35, "Pass 2: Separating detailed stems (GPU)...") try: result = separate_stems(audio_path, progress, two_stem=False) if 'error' in result: error_msg = result['error'] if is_quota_error(str(error_msg)): log.append(QUOTA_ERROR_MSG) else: log.append(f"Pass 2 error: {error_msg}") else: stems_pass2 = result.get('stems', {}) stems_pass2_dir = result.get('stems_dir') log.append(f"Pass 2 stems: {', '.join(stems_pass2.keys())}") except Exception as e: log.append(f"Pass 2 error: {handle_gpu_error(e)}") # === MASTER ORIGINAL (Warm preset) === mastered_path = None progress(0.60, "Mastering original audio (Warm preset)...") try: mastered_output = package_dir / f"{safe_name}_mastered.wav" mastered_path, master_analysis = master_audio( audio_path, str(mastered_output), preset='Warm' ) if mastered_path: log.append(f"Mastered: {Path(mastered_path).name}") if master_analysis and 'output' in master_analysis: lufs = master_analysis['output'].get('lufs', 'N/A') log.append(f" LUFS: {lufs:.1f}" if isinstance(lufs, float) else f" LUFS: {lufs}") except Exception as e: log.append(f"Mastering error: {str(e)}") # === CHORD DETECTION === chords_text = None chords_list = [] chords_file = None progress(0.70, "Detecting chords...") try: if HAS_CHORD_DETECTION: chords_list = extract_chords(audio_path, min_duration=1.0) if chords_list: # Format chord chart - with lyrics alignment if lyrics provided duration = analysis.get('duration', 180) chords_text = format_chord_chart( chords=chords_list, lyrics=lyrics, duration=duration, key=key, bpm=tempo, song_name=song_name ) # Save chord chart chords_file = package_dir / f"{safe_name}_chords.txt" with open(chords_file, 'w') as f: f.write(chords_text) if lyrics: log.append(f"Chord chart: {len(chords_list)} chords aligned to lyrics") else: log.append(f"Chord chart: {len(chords_list)} changes") else: log.append("No chord changes detected") else: log.append("Chord detection not available") except Exception as e: log.append(f"Chord detection error: {str(e)}") # === REAPER PROJECT === reaper_file = None progress(0.80, "Creating Reaper project...") try: if HAS_REAPER_MODULE: # Use Pass 2 stems for Reaper (more detailed), fall back to Pass 1 stems_for_reaper = stems_pass2_dir if stems_pass2 else stems_pass1_dir if stems_for_reaper and Path(stems_for_reaper).exists(): rpp_content = create_reaper_project( song_name=song_name, stems_dir=stems_for_reaper, tempo=tempo, key=key, chords=chords_list, audio_file=audio_path ) reaper_file = package_dir / f"{safe_name}.rpp" with open(reaper_file, 'w') as f: f.write(rpp_content) log.append(f"Reaper project: {reaper_file.name}") else: log.append("Reaper: No stems available for project") else: log.append("Reaper module not available") except Exception as e: log.append(f"Reaper project error: {str(e)}") # === CREATE ZIP BUNDLE === progress(0.90, "Creating zip bundle...") zip_path = OUTPUT_DIR / f"{safe_name}.zip" try: with zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) as zf: files_added = [] # Add original audio zf.write(audio_path, f"{safe_name}/original/{Path(audio_path).name}") files_added.append("original") # Add mastered audio if mastered_path and Path(mastered_path).exists(): zf.write(mastered_path, f"{safe_name}/mastered/{Path(mastered_path).name}") files_added.append("mastered") # Add Pass 1 stems stems1_added = 0 for stem_name, stem_path in stems_pass1.items(): if Path(stem_path).exists(): zf.write(stem_path, f"{safe_name}/stems_basic/{stem_name}.mp3") stems1_added += 1 if stems1_added > 0: files_added.append(f"stems_basic ({stems1_added})") # Add Pass 2 stems stems2_added = 0 for stem_name, stem_path in stems_pass2.items(): if Path(stem_path).exists(): zf.write(stem_path, f"{safe_name}/stems_detailed/{stem_name}.mp3") stems2_added += 1 if stems2_added > 0: files_added.append(f"stems_detailed ({stems2_added})") # Add chord chart if chords_file and Path(chords_file).exists(): zf.write(str(chords_file), f"{safe_name}/{chords_file.name}") files_added.append("chords") # Add Reaper project if reaper_file and Path(reaper_file).exists(): zf.write(str(reaper_file), f"{safe_name}/{reaper_file.name}") files_added.append("reaper") # Add lyrics if provided if lyrics: lyrics_path = package_dir / f"{safe_name}_lyrics.txt" with open(lyrics_path, 'w') as f: f.write(lyrics) zf.write(str(lyrics_path), f"{safe_name}/{lyrics_path.name}") files_added.append("lyrics") log.append(f"\nPackage created: {safe_name}.zip") log.append(f"Contents: {', '.join(files_added)}") log.append(f"Size: {zip_path.stat().st_size / (1024*1024):.1f} MB") except Exception as e: log.append(f"Zip creation error: {str(e)}") zip_path = None # Cleanup temp package directory try: shutil.rmtree(package_dir) except: pass progress(1.0, "Package complete!") return "\n".join(log), str(zip_path) if zip_path else None, get_status_display(user_email) # ============================================================================ # AI STUDIO FUNCTIONS # ============================================================================ @spaces.GPU(duration=300) def _generate_ai_music_gpu(prompt, genre, mood, key, bpm, time_sig, duration, instruments, temperature, user_email, progress=gr.Progress()): """Generate AI music with full parameters (GPU accelerated) - internal""" if not HAS_AUDIOCRAFT: return None, "AudioCraft not installed.\n\nThis feature requires a GPU-enabled environment.\n\nFor local installation with GPU:\npip install audiocraft xformers\npip install torch torchaudio --index-url https://download.pytorch.org/whl/cu118" progress(0.1, "Preparing generation...") # Parse instruments inst_list = [i.strip() for i in instruments.split(',')] if instruments else [] # Handle key selected_key = None if key == "Auto" else key # Generate audio_path, status = generate_song( prompt=prompt, genre=genre, mood=mood, key=selected_key, bpm=int(bpm) if bpm else 120, time_sig=time_sig, duration=float(duration), instruments=inst_list, temperature=temperature, progress_callback=lambda p, m: progress(p, m) ) if audio_path and HAS_CATALOG: # Save to catalog gen_catalog = GeneratedCatalog(user_email or "demo") gen_catalog.add_generated( name=f"Generated_{datetime.now().strftime('%Y%m%d_%H%M%S')}", audio_path=audio_path, prompt=f"{prompt} | {genre} | {mood}", model="musicgen", duration=duration, key=selected_key, bpm=bpm, time_signature=time_sig ) return audio_path, status def generate_ai_music(prompt, genre, mood, key, bpm, time_sig, duration, instruments, temperature, user_email, progress=gr.Progress()): """Generate AI music - wrapper with quota error handling""" try: return _generate_ai_music_gpu(prompt, genre, mood, key, bpm, time_sig, duration, instruments, temperature, user_email, progress) except Exception as e: return None, handle_gpu_error(e) @spaces.GPU(duration=120) def _apply_voice_clone_gpu(audio, voice_model, pitch_shift, user_email, progress=gr.Progress()): """Apply voice cloning/conversion (GPU accelerated) - internal""" if not audio: return None, "Please provide audio" progress(0.2, "Processing voice...") # Use preset if selected if voice_model in PRESET_VOICES: preset = PRESET_VOICES[voice_model] total_pitch = int(pitch_shift) + preset.get('pitch_shift', 0) if HAS_LIBROSA: try: y, sr = librosa.load(audio, sr=None) y_shifted = librosa.effects.pitch_shift(y, sr=sr, n_steps=total_pitch) import soundfile as sf output_path = tempfile.mktemp(suffix='.wav') sf.write(output_path, y_shifted, sr) progress(1.0, "Done!") return output_path, f"Applied {preset.get('name', voice_model)} with {total_pitch} semitone shift" except Exception as e: return None, f"Error: {str(e)}" else: return None, "Librosa required for pitch shifting" # Try RVC if HAS_RVC: try: output, status = clone_voice( source_audio=audio, target_voice=voice_model, pitch_shift=int(pitch_shift), progress_callback=lambda p, m: progress(p, m) ) if is_quota_error(str(status)): return None, QUOTA_ERROR_MSG return output, status except Exception as e: return None, handle_gpu_error(e) return None, "Voice model not found" def apply_voice_clone(audio, voice_model, pitch_shift, user_email, progress=gr.Progress()): """Apply voice cloning - wrapper with quota error handling""" try: return _apply_voice_clone_gpu(audio, voice_model, pitch_shift, user_email, progress) except Exception as e: return None, handle_gpu_error(e) def train_custom_voice(name, description, audio_files, epochs, user_email, progress=gr.Progress()): """Train custom voice model""" if not audio_files: return "Please provide training audio files" if not HAS_RVC: return f"RVC not available.\n\n{RVC_INFO}\n\nFor full RVC support, use the local installation." progress(0.1, "Preparing training data...") # Get file paths file_paths = [] for f in audio_files: if hasattr(f, 'name'): file_paths.append(f.name) elif isinstance(f, str): file_paths.append(f) model_id, status = train_voice_model( name=name, training_files=file_paths, description=description, epochs=int(epochs), user_email=user_email or "demo", progress_callback=lambda p, m: progress(p, m) ) return status # ============================================================================ # CATALOG FUNCTIONS # ============================================================================ def get_catalog_list(user_email): """Get catalog items for display""" if not HAS_CATALOG: return [["Catalog not available", "-", "-", "-", "-"]] items = get_all_audio_for_user(user_email or "demo") data = [] for item in items[:50]: data.append([ item.get('name', 'Unknown'), item.get('type', 'song'), item.get('key', '-'), str(item.get('bpm', '-')), item.get('created', '')[:10] ]) if not data: data = [["No items in catalog", "-", "-", "-", "-"]] return data # ============================================================================ # AUTH FUNCTIONS # ============================================================================ def login_user(email, password): """Login user""" if not email or not password: return "Enter email and password", get_status_display(""), "" success, user = user_manager.login(email, password) if success: return f"Welcome back, {user['name']}!", get_status_display(email), email return "Invalid credentials", get_status_display(""), "" def register_user(email, password, name): """Register new user""" success, msg = user_manager.create_account(email, password, name) if success: return msg, get_status_display(email), email return msg, get_status_display(""), "" def activate_license(email, license_key): """Activate license""" if not email: return "Please login first", get_status_display("") success, msg = user_manager.activate_license(email, license_key) return msg, get_status_display(email) # ============================================================================ # BUILD UI # ============================================================================ with gr.Blocks(css=CSS, title=f"VYNL v{VERSION}") as demo: current_user = gr.State("") # Header gr.HTML(f"""

VYNL

Complete Music Production Suite v{VERSION}

""") # Status status_display = gr.HTML('
DEMO MODE | 3 free processes available
') # Account Section with gr.Accordion("Account / License", open=False): with gr.Row(): with gr.Column(): login_email = gr.Textbox(label="Email") login_pass = gr.Textbox(label="Password", type="password") with gr.Row(): login_btn = gr.Button("Login", variant="primary", size="sm") reg_btn = gr.Button("Register", size="sm") auth_msg = gr.Textbox(label="Status", interactive=False) with gr.Column(): lic_key = gr.Textbox(label="License Key", placeholder="VYNL-XXXX-XXXX-XXXX-XXXX") lic_btn = gr.Button("Activate License") lic_msg = gr.Textbox(label="License Status", interactive=False) # License Agreement with gr.Accordion("License Agreement", open=False): gr.Markdown(""" ### VYNL Software License Agreement **Copyright (c) 2024-2026 Robert T. Lackey. All rights reserved.** By using this software, you agree to the following terms: - All output files (stems, charts, generated audio) are yours to use commercially - The software itself remains the property of Robert T. Lackey - You may NOT redistribute, sublicense, or reverse engineer this software **Stone and Lantern Music Group** Contact: rtlackey@icloud.com """) # Main Tabs with gr.Tabs(): # ========== PROCESS ========== with gr.Tab("PROCESS"): gr.Markdown("### Analyze, Separate Stems, Detect Chords") with gr.Row(): with gr.Column(): proc_audio = gr.Audio(label="Upload Audio", type="filepath") proc_yt = gr.Textbox(label="Or YouTube URL") proc_name = gr.Textbox(label="Song Name (optional)") proc_lyrics = gr.Textbox(label="Lyrics (optional)", lines=3) with gr.Row(): proc_stems = gr.Checkbox(label="Stems", value=True) proc_chords = gr.Checkbox(label="Chords", value=True) proc_daw = gr.Checkbox(label="DAW Project", value=False) with gr.Row(): proc_btn = gr.Button("PROCESS", variant="primary") package_btn = gr.Button("CREATE FULL PACKAGE", variant="secondary") gr.Markdown(""" **PROCESS**: Quick analysis with selected options
**CREATE FULL PACKAGE**: Complete ZIP with all stems (2-pass), mastered audio, chords, Reaper project
""") with gr.Column(): proc_log = gr.Textbox(label="Output Log", lines=15, interactive=False) proc_output = gr.File(label="Download") # ========== AI STUDIO ========== with gr.Tab("AI STUDIO"): gr.Markdown("### AI Music Generation & Voice Cloning") with gr.Tabs(): # Generate Music with gr.Tab("Generate Music"): with gr.Row(): with gr.Column(): gen_prompt = gr.Textbox(label="Describe your music", lines=2, placeholder="Upbeat electronic track with driving bass and atmospheric synths...") with gr.Row(): gen_genre = gr.Dropdown(choices=[""] + GENRES, label="Genre", value="") gen_mood = gr.Dropdown(choices=[""] + MOODS, label="Mood", value="") with gr.Row(): gen_key = gr.Dropdown(choices=["Auto"] + KEYS, label="Key", value="Auto") gen_bpm = gr.Slider(60, 200, value=120, step=1, label="BPM") with gr.Row(): gen_time = gr.Dropdown(choices=TIME_SIGNATURES, label="Time Signature", value="4/4") gen_duration = gr.Slider(10, 600, value=60, step=10, label="Duration (seconds) - up to 10 min") gen_instruments = gr.Textbox(label="Instruments (comma-separated)", placeholder="Piano, Drums, Bass, Synth") gen_temp = gr.Slider(0.5, 1.5, value=1.0, step=0.1, label="Creativity (Temperature)") gen_btn = gr.Button("GENERATE MUSIC", variant="primary") with gr.Column(): gen_output = gr.Audio(label="Generated Audio") gen_status = gr.Textbox(label="Status", lines=5, interactive=False) gr.Markdown(""" **Note:** AI music generation requires GPU and AudioCraft. For full features, use the local installation. """) # Voice Cloning with gr.Tab("Voice Cloning"): with gr.Row(): with gr.Column(): voice_audio = gr.Audio(label="Source Audio (vocals to convert)", type="filepath") voice_choices = list(PRESET_VOICES.keys()) voice_model = gr.Dropdown(choices=voice_choices, label="Voice Model/Preset", value=voice_choices[0] if voice_choices else None) voice_pitch = gr.Slider(-24, 24, value=0, step=1, label="Additional Pitch Shift (semitones)") voice_btn = gr.Button("CONVERT VOICE", variant="primary") with gr.Column(): voice_output = gr.Audio(label="Converted Output") voice_status = gr.Textbox(label="Status", lines=3, interactive=False) gr.Markdown(""" **Available Presets:** - **Male Tenor** - Standard male voice - **Male Bass** - Deep male voice (-5 semitones) - **Female Alto** - Standard female voice (+12 semitones) - **Female Soprano** - High female voice (+15 semitones) """) # Train Voice with gr.Tab("Train Custom Voice"): with gr.Row(): with gr.Column(): train_name = gr.Textbox(label="Voice Model Name", placeholder="My Custom Voice") train_desc = gr.Textbox(label="Description", placeholder="Describe the voice characteristics") train_files = gr.File(label="Training Audio Files (upload multiple clean vocal recordings)", file_count="multiple", file_types=["audio"]) train_epochs = gr.Slider(50, 500, value=100, step=50, label="Training Epochs") train_btn = gr.Button("START TRAINING", variant="primary") with gr.Column(): train_status = gr.Textbox(label="Training Status", lines=12, interactive=False) gr.Markdown(""" **Training Tips:** - Use 10-30 minutes of clean vocal recordings - Avoid background music or noise - Include variety (different pitches, vowels) - More epochs = better quality (but takes longer) **Note:** Full voice training requires local GPU installation. """) # ========== MASTER ========== with gr.Tab("MASTER"): gr.Markdown("### AI Mastering") with gr.Row(): with gr.Column(): master_input = gr.Audio(label="Input (Unmastered Mix)", type="filepath") master_ref = gr.Audio(label="Reference Track (optional)", type="filepath") master_lufs = gr.Slider(-20, -8, value=-14, step=0.5, label="Target LUFS") master_preset = gr.Radio( ["Balanced", "Warm", "Bright", "Punchy", "Reference Match"], label="Mastering Preset", value="Balanced" ) master_btn = gr.Button("MASTER", variant="primary") with gr.Column(): master_output = gr.Audio(label="Mastered Output") master_status = gr.Textbox(label="Analysis Report", lines=10, interactive=False) # ========== CATALOG ========== with gr.Tab("CATALOG"): gr.Markdown("### Your Music Library - All processed and generated audio") cat_refresh = gr.Button("Refresh Catalog") cat_table = gr.Dataframe( headers=["Name", "Type", "Key", "BPM", "Date"], label="Your Music", interactive=False ) # ========== SESSIONS ========== with gr.Tab("SESSIONS"): gr.Markdown("### Setlist Management & Teleprompter") with gr.Row(): with gr.Column(): sess_name = gr.Textbox(label="Session/Setlist Name") sess_songs = gr.Textbox(label="Songs (one per line)", lines=12, placeholder="Song 1\nSong 2\nSong 3...") sess_save = gr.Button("Save Session") with gr.Column(): gr.Markdown("### Teleprompter Preview") sess_display = gr.HTML("""

Load a session to display teleprompter

""") # Footer gr.HTML(f""" """) # ========== WIRE EVENTS ========== # Auth login_btn.click(login_user, [login_email, login_pass], [auth_msg, status_display, current_user]) reg_btn.click(register_user, [login_email, login_pass, login_email], [auth_msg, status_display, current_user]) lic_btn.click(activate_license, [current_user, lic_key], [lic_msg, status_display]) # Process proc_btn.click( process_song, [proc_audio, proc_yt, proc_name, proc_lyrics, proc_stems, proc_chords, proc_daw, current_user], [proc_log, proc_output, status_display] ) # Create Full Package package_btn.click( create_song_package, [proc_audio, proc_yt, proc_name, proc_lyrics, current_user], [proc_log, proc_output, status_display] ) # AI Studio - Generate gen_btn.click( generate_ai_music, [gen_prompt, gen_genre, gen_mood, gen_key, gen_bpm, gen_time, gen_duration, gen_instruments, gen_temp, current_user], [gen_output, gen_status] ) # AI Studio - Voice Clone voice_btn.click( apply_voice_clone, [voice_audio, voice_model, voice_pitch, current_user], [voice_output, voice_status] ) # AI Studio - Train Voice train_btn.click( train_custom_voice, [train_name, train_desc, train_files, train_epochs, current_user], [train_status] ) # Master def master_track(input_audio, ref_audio, lufs, preset, user): if not input_audio: return None, "Please provide audio to master" try: output, analysis = master_audio(input_audio, ref_audio, lufs, preset) return output, format_analysis(analysis) if analysis else "Mastering complete" except Exception as e: return None, f"Mastering error: {str(e)}" master_btn.click( master_track, [master_input, master_ref, master_lufs, master_preset, current_user], [master_output, master_status] ) # Catalog cat_refresh.click(get_catalog_list, [current_user], [cat_table]) # ============================================================================ # LAUNCH # ============================================================================ if __name__ == "__main__": print(f"\n{'='*60}") print(f"VYNL v{VERSION} - HuggingFace Edition") print(f"Copyright (c) 2024-2026 Robert T. Lackey") print(f"{'='*60}") print(f"\nSystem Status:") print(f" AudioCraft: {'Available' if HAS_AUDIOCRAFT else 'Not available'}") print(f" RVC: {'Available' if HAS_RVC else 'Not available'}") print(f" Librosa: {'Available' if HAS_LIBROSA else 'Not installed'}") print(f" yt-dlp: {'Available' if HAS_YTDLP else 'Not installed'}") print(f" Catalog: {'Available' if HAS_CATALOG else 'Not available'}") print(f"\nStarting server at http://localhost:7860") print(f"{'='*60}\n") demo.launch(server_name="0.0.0.0", server_port=7860)