add initial vllm inf script

This commit is contained in:
slSeanWU
2025-10-30 11:28:52 -04:00
parent 406dd0b7c7
commit 2b9d0a8227
6 changed files with 743 additions and 1 deletions
+220
View File
@@ -0,0 +1,220 @@
"""
Utility functions for MIDI-LLM.
This module contains helper functions for audio synthesis, MIDI conversion,
and other supporting operations. Users can safely skip this file when learning
the codebase - start with generate_vllm.py or train.py instead.
"""
import os
import sys
from pathlib import Path
from typing import List, Optional, Union
import torch
# Core dependency - required
try:
from anticipation.convert import events_to_midi
except ImportError:
print("Error: anticipation package not found. Please install it for MIDI conversion.")
print("Install with: pip install anticipation")
sys.exit(1)
# Optional dependencies for audio synthesis
SYNTHESIS_AVAILABLE = False
try:
import midi2audio
import librosa
import librosa.effects
import soundfile as sf
SYNTHESIS_AVAILABLE = True
except ImportError:
pass
# ============================================================================
# Constants
# ============================================================================
AMT_GPT2_BOS_ID = 55026
LLAMA_VOCAB_SIZE = 128256
LLAMA_MODEL_NAME = "meta-llama/Llama-3.2-1B"
# MIDI tokens are in the extended vocabulary range
ALLOWED_TOKEN_IDS = list(range(LLAMA_VOCAB_SIZE, LLAMA_VOCAB_SIZE + AMT_GPT2_BOS_ID))
# ============================================================================
# Validation
# ============================================================================
def has_excessive_notes_at_any_time(
tokens: Union[torch.Tensor, List[int]],
max_notes_per_time: int = 64
) -> bool:
"""
Check if generated MIDI has excessive simultaneous notes at any time point.
This validation helps filter out invalid or unrealistic generations that have
too many notes playing at once, which can indicate a failure mode.
Args:
tokens: Token sequence (torch.Tensor or list of ints)
max_notes_per_time: Maximum allowed notes at any single time point
Returns:
True if excessive notes detected, False otherwise
"""
# Convert to tensor if needed
if isinstance(tokens, list):
tokens = torch.tensor(tokens)
# Extract time tokens (every 3rd token in the sequence: time, duration, note)
times = tokens[::3]
# Use torch.bincount for efficient counting
# bincount returns counts for indices 0 to max_value
counts = torch.bincount(times)
# Check if any time has more than max_notes_per_time notes
return torch.any(counts > max_notes_per_time).item()
# ============================================================================
# Audio Synthesis
# ============================================================================
def synthesize_midi_to_audio(
midi_path: str,
soundfont_path: str,
save_mp3: bool = True,
samplerate: Optional[int] = None
) -> bool:
"""
Synthesize MIDI file to audio (WAV/MP3) using FluidSynth.
Args:
midi_path: Path to MIDI file
soundfont_path: Path to SoundFont (.sf2) file
save_mp3: If True, convert to MP3 and delete WAV
samplerate: Optional sample rate for audio
Returns:
True if successful, False otherwise
"""
if not SYNTHESIS_AVAILABLE:
print("Warning: Audio synthesis libraries not available. Skipping synthesis.")
print("Install with: conda install conda-forge::fluidsynth conda-forge::ffmpeg")
print(" pip install midi2audio librosa soundfile")
return False
try:
wav_path = midi_path.replace(".mid", ".wav")
# Initialize FluidSynth
fs = midi2audio.FluidSynth(soundfont_path)
if samplerate is not None:
fs.sample_rate = samplerate
# Synthesize MIDI to WAV
fs.midi_to_audio(midi_path, wav_path)
# Trim silence from audio
wav, sr = librosa.load(wav_path)
wav, _ = librosa.effects.trim(wav, top_db=30)
sf.write(wav_path, wav, sr)
if save_mp3:
# Convert WAV to MP3 using ffmpeg
mp3_path = midi_path.replace(".mid", ".mp3")
if samplerate is None:
cmd = f"ffmpeg -i {wav_path} -codec:a libmp3lame -qscale:a 2 {mp3_path} -y >/dev/null 2>&1"
else:
cmd = f"ffmpeg -i {wav_path} -codec:a libmp3lame -qscale:a 2 -ar {samplerate} {mp3_path} -y >/dev/null 2>&1"
os.system(cmd)
# Remove WAV file
if os.path.exists(wav_path):
os.remove(wav_path)
return True
except Exception as e:
print(f"Error synthesizing MIDI to audio: {e}")
return False
# ============================================================================
# MIDI Generation and Saving
# ============================================================================
def save_generation(
tokens: List[int],
prompt: str,
output_dir: Path,
generation_idx: int,
soundfont_path: Optional[str] = None,
synthesize: bool = False,
validate: bool = True
) -> bool:
"""
Save generated tokens as MIDI file (and optionally audio).
Args:
tokens: List of generated token IDs (already shifted from LLAMA vocab)
prompt: Original text prompt
output_dir: Directory to save outputs
generation_idx: Index of this generation (for multiple outputs)
soundfont_path: Path to SoundFont file for synthesis
synthesize: Whether to synthesize to audio
validate: Whether to validate tokens before saving (checks for excessive notes)
Returns:
True if successful, False otherwise
"""
try:
# Validate tokens before saving
if validate:
if has_excessive_notes_at_any_time(tokens, max_notes_per_time=64):
print(f" ✗ Generation {generation_idx}: Failed validation (excessive simultaneous notes)")
return False
# Create output directory
output_dir.mkdir(parents=True, exist_ok=True)
# Save prompt text
prompt_file = output_dir / "prompt.txt"
with open(prompt_file, "w") as f:
f.write(prompt)
# Save token sequence
tokens_file = output_dir / f"gen_{generation_idx}_tokens.txt"
with open(tokens_file, "w") as f:
for token in tokens:
f.write(f"{token}\n")
# Convert tokens to MIDI
midi_obj = events_to_midi(tokens)
midi_file = output_dir / f"gen_{generation_idx}.mid"
midi_obj.save(str(midi_file))
print(f" ✓ Saved MIDI: {midi_file}")
# Optionally synthesize to audio
if synthesize and soundfont_path:
success = synthesize_midi_to_audio(
str(midi_file),
soundfont_path,
save_mp3=True
)
if success:
print(f" ✓ Synthesized audio: {midi_file.with_suffix('.mp3')}")
return True
except Exception as e:
print(f" ✗ Error saving generation {generation_idx}: {e}")
return False