import os import sys import json from datetime import datetime from agents import SplitterAgent, EarAgent, TabAgent from suno_postprocessor import process_suno_audio, SunoNotePostprocessor # Fix Windows console encoding for emojis if sys.platform == "win32": import codecs sys.stdout = codecs.getwriter("utf-8")(sys.stdout.detach()) def load_user_memory(): """Load user memory and extract preferences.""" # In Docker: /app/user_memory # On Windows/Local: ./user_memory if os.path.exists("/app") and not sys.platform.startswith("win"): memory_dir = "/app/user_memory" else: memory_dir = "./user_memory" os.makedirs(memory_dir, exist_ok=True) memory_file = os.path.join(memory_dir, "user_preferences.json") # Default configurations config = { "bass_tuning": [23, 28, 33, 38, 43], # B0-E1-A1-D2-G2 (5-string) "guitar_tuning": [40, 45, 50, 55, 59, 64], # E2-A2-D3-G3-B3-E4 (standard) "bass_num_strings": 5, "guitar_num_strings": 6, "num_frets": 24, "prefer_low_strings": True, } # Load from file if it exists if os.path.exists(memory_file): try: with open(memory_file, 'r') as f: preferences = json.load(f) if "config" in preferences: config.update(preferences["config"]) print(f"🧠 Loaded preferences from: {memory_file}") except Exception as e: print(f"āš ļø Warning: Could not load preferences: {e}") print(" Using default configuration") else: print("šŸ“‹ Using default configuration (run init_memory.py to customize)") return memory_file, config def export_tab_to_txt(tab_data, output_path, instrument="Guitar"): """Export tablature to human-readable text format.""" if not tab_data: print(f"āš ļø No tab data to export for {instrument}") return num_strings = max(pos['string'] for pos in tab_data) + 1 # Create ASCII tab grid lines = [[] for _ in range(num_strings)] for pos in tab_data: string_idx = pos['string'] fret = pos['fret'] technique = pos.get('technique', 'pick') # Format fret number with technique marker if technique == "slide": fret_str = f"{fret}s" elif technique == "hammer": fret_str = f"{fret}h" elif technique == "pull": fret_str = f"{fret}p" else: fret_str = str(fret) # Add to appropriate string for i in range(num_strings): if i == string_idx: lines[i].append(fret_str.ljust(3)) else: lines[i].append("---") # Write to file with open(output_path, 'w') as f: f.write(f"=== {instrument} Tablature ===\n\n") # String labels (reverse order for display) string_labels = ["E", "A", "D", "G", "B", "E"] if num_strings == 6 else ["B", "E", "A", "D", "G"] for i in range(num_strings - 1, -1, -1): f.write(f"{string_labels[i]}|{''.join(lines[i])}|\n") f.write(f"\nLegend: s=slide, h=hammer-on, p=pull-off\n") f.write(f"Generated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n") print(f"šŸ“„ Saved tab: {output_path}") def export_tab_to_json(tab_data, output_path, instrument="Guitar"): """Export tablature to JSON format for programmatic use.""" data = { "instrument": instrument, "timestamp": datetime.now().isoformat(), "tablature": tab_data } with open(output_path, 'w') as f: json.dump(data, f, indent=2) print(f"šŸ“„ Saved JSON: {output_path}") def main(): print("=" * 60) print("šŸŽø TAB AGENT - Audio to Tablature Pipeline") print("=" * 60) # Parse arguments if len(sys.argv) < 2: print("Usage: python main.py ") sys.exit(1) song_file = sys.argv[1] # Determine paths (Docker vs local) if os.path.exists("/app/input"): input_dir = "/app/input" output_dir = "/app/output" else: input_dir = "./input" output_dir = "./output" os.makedirs(input_dir, exist_ok=True) os.makedirs(output_dir, exist_ok=True) audio_path = os.path.join(input_dir, song_file) if not os.path.exists(audio_path): print(f"āŒ Error: Audio file not found: {audio_path}") sys.exit(1) song_name = os.path.splitext(song_file)[0] print(f"\nšŸŽµ Processing: {song_file}") print(f"šŸ“‚ Input: {input_dir}") print(f"šŸ“‚ Output: {output_dir}\n") # Load user memory and configuration memory_file, config = load_user_memory() # Suno artifact detection and preprocessing print("\n" + "=" * 60) print("STAGE 0: AUDIO QUALITY ANALYSIS") print("=" * 60) processed_audio, is_suno, suno_metrics = process_suno_audio( audio_path, output_path=os.path.join(output_dir, f"{song_name}_processed.wav") ) # Adjust transcription parameters based on audio quality if is_suno: onset_threshold = 0.6 # Higher threshold for noisy AI audio frame_threshold = 0.4 else: onset_threshold = 0.5 # Standard for clean audio frame_threshold = 0.3 # Initialize agents print("\n" + "=" * 60) print("STAGE 1-3: STEM SEPARATION & PROCESSING") print("=" * 60) splitter = SplitterAgent(output_dir=os.path.join(output_dir, "stems")) # Separate stems (use processed audio if Suno) stems = splitter.separate_stems(processed_audio) # Process guitar stems guitar_stems = splitter.process_guitars(stems['guitar']) # Process bass stem bass_clean = splitter.process_bass(stems['bass']) # Initialize transcription agent print("\n" + "=" * 60) print("STAGE 4: AUDIO TRANSCRIPTION") print("=" * 60) ear = EarAgent() suno_postprocessor = SunoNotePostprocessor() # Transcribe lead guitar print("\nšŸŽø Transcribing Lead Guitar...") lead_notes_raw = ear.transcribe_stem( guitar_stems['lead'], target="Lead Guitar", onset_threshold=onset_threshold, frame_threshold=frame_threshold ) lead_notes = ear.humanize_and_clean(lead_notes_raw, is_bass=False) # Apply Suno post-processing if needed lead_notes = suno_postprocessor.process(lead_notes, is_suno, suno_metrics) lead_midi_path = os.path.join(output_dir, f"{song_name}_lead_guitar.mid") ear.export_midi(lead_notes, lead_midi_path) # Transcribe rhythm guitar (left channel) print("\nšŸŽø Transcribing Rhythm Guitar (L)...") rhythm_l_notes_raw = ear.transcribe_stem( guitar_stems['left'], target="Rhythm Guitar L", onset_threshold=onset_threshold, frame_threshold=frame_threshold ) rhythm_l_notes = ear.humanize_and_clean(rhythm_l_notes_raw, is_bass=False) rhythm_l_notes = suno_postprocessor.process(rhythm_l_notes, is_suno, suno_metrics) rhythm_l_midi_path = os.path.join(output_dir, f"{song_name}_rhythm_L.mid") ear.export_midi(rhythm_l_notes, rhythm_l_midi_path) # Transcribe rhythm guitar (right channel) print("\nšŸŽø Transcribing Rhythm Guitar (R)...") rhythm_r_notes_raw = ear.transcribe_stem( guitar_stems['right'], target="Rhythm Guitar R", onset_threshold=onset_threshold, frame_threshold=frame_threshold ) rhythm_r_notes = ear.humanize_and_clean(rhythm_r_notes_raw, is_bass=False) rhythm_r_notes = suno_postprocessor.process(rhythm_r_notes, is_suno, suno_metrics) rhythm_r_midi_path = os.path.join(output_dir, f"{song_name}_rhythm_R.mid") ear.export_midi(rhythm_r_notes, rhythm_r_midi_path) # Transcribe bass print("\nšŸŽø Transcribing Bass...") bass_notes_raw = ear.transcribe_stem( bass_clean, target="Bass", onset_threshold=onset_threshold, frame_threshold=frame_threshold ) bass_notes = ear.humanize_and_clean(bass_notes_raw, is_bass=True) bass_notes = suno_postprocessor.process(bass_notes, is_suno, suno_metrics) bass_midi_path = os.path.join(output_dir, f"{song_name}_bass.mid") ear.export_midi(bass_notes, bass_midi_path) # Generate tablature print("\n" + "=" * 60) print("STAGE 5: TABLATURE GENERATION") print("=" * 60) # Guitar tablature guitar_agent = TabAgent( tuning=config['guitar_tuning'], num_frets=config['num_frets'] ) print("\nšŸŽø Generating Lead Guitar Tab...") lead_tab = guitar_agent.generate_tab(lead_notes) export_tab_to_txt(lead_tab, os.path.join(output_dir, f"{song_name}_lead_guitar.tab"), "Lead Guitar") export_tab_to_json(lead_tab, os.path.join(output_dir, f"{song_name}_lead_guitar.json"), "Lead Guitar") print("\nšŸŽø Generating Rhythm Guitar (L) Tab...") rhythm_l_tab = guitar_agent.generate_tab(rhythm_l_notes) export_tab_to_txt(rhythm_l_tab, os.path.join(output_dir, f"{song_name}_rhythm_L.tab"), "Rhythm Guitar L") export_tab_to_json(rhythm_l_tab, os.path.join(output_dir, f"{song_name}_rhythm_L.json"), "Rhythm Guitar L") print("\nšŸŽø Generating Rhythm Guitar (R) Tab...") rhythm_r_tab = guitar_agent.generate_tab(rhythm_r_notes) export_tab_to_txt(rhythm_r_tab, os.path.join(output_dir, f"{song_name}_rhythm_R.tab"), "Rhythm Guitar R") export_tab_to_json(rhythm_r_tab, os.path.join(output_dir, f"{song_name}_rhythm_R.json"), "Rhythm Guitar R") # Bass tablature bass_agent = TabAgent( tuning=config['bass_tuning'], num_frets=config['num_frets'] ) print("\nšŸŽø Generating Bass Tab...") bass_tab = bass_agent.generate_tab(bass_notes) export_tab_to_txt(bass_tab, os.path.join(output_dir, f"{song_name}_bass.tab"), "5-String Bass") export_tab_to_json(bass_tab, os.path.join(output_dir, f"{song_name}_bass.json"), "5-String Bass") # Log session to memory if os.path.exists(memory_file): try: with open(memory_file, 'r') as f: preferences = json.load(f) session_log = { "timestamp": datetime.now().isoformat(), "song": song_file, "status": "completed" } preferences.setdefault("sessions", []).append(session_log) with open(memory_file, 'w') as f: json.dump(preferences, f, indent=2) except Exception as e: print(f"āš ļø Could not log session: {e}") # Summary print("\n" + "=" * 60) print("āœ… PIPELINE COMPLETE") print("=" * 60) print(f"\nšŸ“ Output Files:") print(f" MIDI Files:") print(f" - {song_name}_lead_guitar.mid") print(f" - {song_name}_rhythm_L.mid") print(f" - {song_name}_rhythm_R.mid") print(f" - {song_name}_bass.mid") print(f"\n Tablature Files:") print(f" - {song_name}_lead_guitar.tab") print(f" - {song_name}_rhythm_L.tab") print(f" - {song_name}_rhythm_R.tab") print(f" - {song_name}_bass.tab") print(f"\n JSON Files:") print(f" - {song_name}_lead_guitar.json") print(f" - {song_name}_rhythm_L.json") print(f" - {song_name}_rhythm_R.json") print(f" - {song_name}_bass.json") print(f"\nšŸ“‚ Location: {output_dir}") print("\n" + "=" * 60) if __name__ == "__main__": main()