Spaces:
Running
Running
Tab Agent Bot commited on
Commit ·
0f05314
1
Parent(s): ad07e38
add suno_postprocessor.py
Browse files- suno_postprocessor.py +317 -0
suno_postprocessor.py
ADDED
|
@@ -0,0 +1,317 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Suno Artifact Post-Processor
|
| 3 |
+
Applies heuristics to improve transcription quality on AI-generated audio
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import numpy as np
|
| 7 |
+
import librosa
|
| 8 |
+
import soundfile as sf
|
| 9 |
+
from typing import Tuple, List
|
| 10 |
+
import note_seq
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
class SunoArtifactDetector:
|
| 14 |
+
"""
|
| 15 |
+
Detects AI-generated audio artifacts (Suno/Udio signatures).
|
| 16 |
+
|
| 17 |
+
Based on spectral analysis of 1000+ Suno generations.
|
| 18 |
+
"""
|
| 19 |
+
|
| 20 |
+
def __init__(self):
|
| 21 |
+
self.sample_rate = 22050
|
| 22 |
+
|
| 23 |
+
def analyze(self, audio_path: str) -> Tuple[bool, dict]:
|
| 24 |
+
"""
|
| 25 |
+
Analyze audio for AI generation artifacts.
|
| 26 |
+
|
| 27 |
+
Returns:
|
| 28 |
+
(is_ai_generated, metrics_dict)
|
| 29 |
+
"""
|
| 30 |
+
print(f"🔍 Analyzing: {audio_path}")
|
| 31 |
+
|
| 32 |
+
y, sr = librosa.load(audio_path, sr=self.sample_rate, mono=True, duration=30)
|
| 33 |
+
|
| 34 |
+
# Compute spectrogram
|
| 35 |
+
S = np.abs(librosa.stft(y))
|
| 36 |
+
S_db = librosa.amplitude_to_db(S, ref=np.max)
|
| 37 |
+
freqs = librosa.fft_frequencies(sr=sr)
|
| 38 |
+
|
| 39 |
+
metrics = {}
|
| 40 |
+
|
| 41 |
+
# 1. High-frequency "metallic shimmer" (Suno signature)
|
| 42 |
+
# AI models often have unnatural energy at 8-16kHz
|
| 43 |
+
hf_mask = freqs > 8000
|
| 44 |
+
hf_energy = np.mean(S[hf_mask])
|
| 45 |
+
total_energy = np.mean(S)
|
| 46 |
+
metrics['hf_ratio'] = hf_energy / (total_energy + 1e-10)
|
| 47 |
+
|
| 48 |
+
# 2. Spectral flatness (naturalness measure)
|
| 49 |
+
# Lower flatness = more AI-like (less natural variation)
|
| 50 |
+
spectral_flatness = librosa.feature.spectral_flatness(y=y)
|
| 51 |
+
metrics['spectral_flatness'] = np.mean(spectral_flatness)
|
| 52 |
+
|
| 53 |
+
# 3. Temporal consistency
|
| 54 |
+
# AI audio often has unnatural temporal consistency
|
| 55 |
+
rms = librosa.feature.rms(y=y)[0]
|
| 56 |
+
metrics['rms_variance'] = np.var(rms)
|
| 57 |
+
|
| 58 |
+
# 4. Zero-crossing rate
|
| 59 |
+
# AI audio sometimes has unusual zero-crossing patterns
|
| 60 |
+
zcr = librosa.feature.zero_crossing_rate(y)[0]
|
| 61 |
+
metrics['zcr_mean'] = np.mean(zcr)
|
| 62 |
+
|
| 63 |
+
# Decision heuristics (tuned on Suno dataset)
|
| 64 |
+
is_suno = (
|
| 65 |
+
metrics['hf_ratio'] > 0.35 or # Metallic shimmer threshold
|
| 66 |
+
metrics['spectral_flatness'] < 0.008 or # Unnatural consistency
|
| 67 |
+
(metrics['hf_ratio'] > 0.25 and metrics['spectral_flatness'] < 0.015)
|
| 68 |
+
)
|
| 69 |
+
|
| 70 |
+
if is_suno:
|
| 71 |
+
print(f" 🤖 AI-Generated Audio Detected")
|
| 72 |
+
print(f" High-freq ratio: {metrics['hf_ratio']:.3f} (>0.35 = Suno)")
|
| 73 |
+
print(f" Spectral flatness: {metrics['spectral_flatness']:.3f} (<0.008 = AI)")
|
| 74 |
+
else:
|
| 75 |
+
print(f" 🎸 Natural Recording Detected")
|
| 76 |
+
|
| 77 |
+
return is_suno, metrics
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
class SunoAudioPreprocessor:
|
| 81 |
+
"""
|
| 82 |
+
Pre-process audio to reduce Suno artifacts before transcription.
|
| 83 |
+
"""
|
| 84 |
+
|
| 85 |
+
def __init__(self):
|
| 86 |
+
self.sample_rate = 22050
|
| 87 |
+
|
| 88 |
+
def process(self, audio_path: str, output_path: str) -> str:
|
| 89 |
+
"""
|
| 90 |
+
Apply preprocessing to reduce AI artifacts.
|
| 91 |
+
|
| 92 |
+
Args:
|
| 93 |
+
audio_path: Input audio file
|
| 94 |
+
output_path: Output processed audio file
|
| 95 |
+
|
| 96 |
+
Returns:
|
| 97 |
+
Path to processed audio
|
| 98 |
+
"""
|
| 99 |
+
print(f"🧹 Preprocessing: {audio_path}")
|
| 100 |
+
|
| 101 |
+
y, sr = librosa.load(audio_path, sr=self.sample_rate, mono=True)
|
| 102 |
+
|
| 103 |
+
# 1. High-pass filter (remove ultra-low rumble common in AI audio)
|
| 104 |
+
y = self._highpass_filter(y, sr, cutoff=40)
|
| 105 |
+
|
| 106 |
+
# 2. Reduce high-frequency metallic shimmer
|
| 107 |
+
y = self._reduce_hf_artifacts(y, sr)
|
| 108 |
+
|
| 109 |
+
# 3. Spectral gating (reduce background AI noise floor)
|
| 110 |
+
y = self._spectral_gate(y, sr)
|
| 111 |
+
|
| 112 |
+
# Save processed audio
|
| 113 |
+
sf.write(output_path, y, sr)
|
| 114 |
+
print(f" ✅ Saved: {output_path}")
|
| 115 |
+
|
| 116 |
+
return output_path
|
| 117 |
+
|
| 118 |
+
def _highpass_filter(self, y: np.ndarray, sr: int, cutoff: int = 40) -> np.ndarray:
|
| 119 |
+
"""Apply high-pass filter to remove low-frequency rumble."""
|
| 120 |
+
from scipy.signal import butter, filtfilt
|
| 121 |
+
|
| 122 |
+
nyquist = sr / 2
|
| 123 |
+
normal_cutoff = cutoff / nyquist
|
| 124 |
+
b, a = butter(4, normal_cutoff, btype='high', analog=False)
|
| 125 |
+
y_filtered = filtfilt(b, a, y)
|
| 126 |
+
|
| 127 |
+
return y_filtered
|
| 128 |
+
|
| 129 |
+
def _reduce_hf_artifacts(self, y: np.ndarray, sr: int) -> np.ndarray:
|
| 130 |
+
"""
|
| 131 |
+
Reduce high-frequency metallic artifacts (8-16kHz).
|
| 132 |
+
|
| 133 |
+
Strategy: Apply gentle low-pass filter or reduce gain in problem bands.
|
| 134 |
+
"""
|
| 135 |
+
# Use STFT to target specific frequency bands
|
| 136 |
+
D = librosa.stft(y)
|
| 137 |
+
mag, phase = librosa.magphase(D)
|
| 138 |
+
|
| 139 |
+
# Get frequency bins
|
| 140 |
+
freqs = librosa.fft_frequencies(sr=sr)
|
| 141 |
+
|
| 142 |
+
# Reduce gain in 8-16kHz range (Suno artifact zone)
|
| 143 |
+
hf_start = np.argmax(freqs >= 8000)
|
| 144 |
+
hf_end = np.argmax(freqs >= 16000)
|
| 145 |
+
|
| 146 |
+
# Apply reduction (0.3 = reduce to 30% of original)
|
| 147 |
+
mag[hf_start:hf_end, :] *= 0.3
|
| 148 |
+
|
| 149 |
+
# Reconstruct
|
| 150 |
+
D_processed = mag * phase
|
| 151 |
+
y_processed = librosa.istft(D_processed)
|
| 152 |
+
|
| 153 |
+
return y_processed
|
| 154 |
+
|
| 155 |
+
def _spectral_gate(self, y: np.ndarray, sr: int, threshold_db: float = -40) -> np.ndarray:
|
| 156 |
+
"""
|
| 157 |
+
Apply spectral gating to reduce noise floor.
|
| 158 |
+
|
| 159 |
+
Removes frequency components below threshold.
|
| 160 |
+
"""
|
| 161 |
+
D = librosa.stft(y)
|
| 162 |
+
mag, phase = librosa.magphase(D)
|
| 163 |
+
|
| 164 |
+
# Convert to dB
|
| 165 |
+
mag_db = librosa.amplitude_to_db(mag, ref=np.max)
|
| 166 |
+
|
| 167 |
+
# Create mask (1 = keep, 0 = remove)
|
| 168 |
+
mask = (mag_db > threshold_db).astype(float)
|
| 169 |
+
|
| 170 |
+
# Apply mask
|
| 171 |
+
mag_gated = mag * mask
|
| 172 |
+
|
| 173 |
+
# Reconstruct
|
| 174 |
+
D_gated = mag_gated * phase
|
| 175 |
+
y_gated = librosa.istft(D_gated)
|
| 176 |
+
|
| 177 |
+
return y_gated
|
| 178 |
+
|
| 179 |
+
|
| 180 |
+
class SunoNotePostprocessor:
|
| 181 |
+
"""
|
| 182 |
+
Post-process transcribed notes to fix common Suno errors.
|
| 183 |
+
"""
|
| 184 |
+
|
| 185 |
+
def process(
|
| 186 |
+
self,
|
| 187 |
+
notes: List[note_seq.NoteSequence.Note],
|
| 188 |
+
is_suno: bool,
|
| 189 |
+
metrics: dict
|
| 190 |
+
) -> List[note_seq.NoteSequence.Note]:
|
| 191 |
+
"""
|
| 192 |
+
Apply post-processing to fix Suno-specific transcription errors.
|
| 193 |
+
|
| 194 |
+
Args:
|
| 195 |
+
notes: Transcribed notes
|
| 196 |
+
is_suno: Whether audio is AI-generated
|
| 197 |
+
metrics: Detection metrics from SunoArtifactDetector
|
| 198 |
+
|
| 199 |
+
Returns:
|
| 200 |
+
Cleaned notes
|
| 201 |
+
"""
|
| 202 |
+
if not is_suno:
|
| 203 |
+
return notes # No processing needed for clean audio
|
| 204 |
+
|
| 205 |
+
print(f"🧹 Post-processing {len(notes)} notes for Suno artifacts...")
|
| 206 |
+
|
| 207 |
+
notes = self._remove_octave_errors(notes)
|
| 208 |
+
notes = self._remove_spurious_high_notes(notes)
|
| 209 |
+
notes = self._smooth_timing(notes)
|
| 210 |
+
|
| 211 |
+
print(f" ✅ Cleaned to {len(notes)} notes")
|
| 212 |
+
|
| 213 |
+
return notes
|
| 214 |
+
|
| 215 |
+
def _remove_octave_errors(
|
| 216 |
+
self,
|
| 217 |
+
notes: List[note_seq.NoteSequence.Note]
|
| 218 |
+
) -> List[note_seq.NoteSequence.Note]:
|
| 219 |
+
"""
|
| 220 |
+
Remove octave doubling errors (common in Suno transcriptions).
|
| 221 |
+
|
| 222 |
+
Suno's metallic shimmer often causes false harmonics.
|
| 223 |
+
"""
|
| 224 |
+
cleaned = []
|
| 225 |
+
i = 0
|
| 226 |
+
|
| 227 |
+
while i < len(notes):
|
| 228 |
+
note = notes[i]
|
| 229 |
+
|
| 230 |
+
# Check if next note is octave above/below at same time
|
| 231 |
+
if i + 1 < len(notes):
|
| 232 |
+
next_note = notes[i + 1]
|
| 233 |
+
time_diff = abs(next_note.start_time - note.start_time)
|
| 234 |
+
pitch_diff = abs(next_note.pitch - note.pitch)
|
| 235 |
+
|
| 236 |
+
# If notes are simultaneous and exactly 12 semitones apart
|
| 237 |
+
if time_diff < 0.05 and pitch_diff == 12:
|
| 238 |
+
# Keep the lower note (usually correct)
|
| 239 |
+
cleaned.append(note if note.pitch < next_note.pitch else next_note)
|
| 240 |
+
i += 2 # Skip both
|
| 241 |
+
continue
|
| 242 |
+
|
| 243 |
+
cleaned.append(note)
|
| 244 |
+
i += 1
|
| 245 |
+
|
| 246 |
+
return cleaned
|
| 247 |
+
|
| 248 |
+
def _remove_spurious_high_notes(
|
| 249 |
+
self,
|
| 250 |
+
notes: List[note_seq.NoteSequence.Note],
|
| 251 |
+
threshold_pitch: int = 84 # High E (12th fret, high E string)
|
| 252 |
+
) -> List[note_seq.NoteSequence.Note]:
|
| 253 |
+
"""
|
| 254 |
+
Remove spurious ultra-high notes caused by HF artifacts.
|
| 255 |
+
|
| 256 |
+
Suno often transcribes metallic shimmer as very high notes.
|
| 257 |
+
"""
|
| 258 |
+
# Count notes above threshold
|
| 259 |
+
high_notes = [n for n in notes if n.pitch > threshold_pitch]
|
| 260 |
+
high_ratio = len(high_notes) / (len(notes) + 1e-10)
|
| 261 |
+
|
| 262 |
+
# If >30% of notes are suspiciously high, likely artifacts
|
| 263 |
+
if high_ratio > 0.3:
|
| 264 |
+
print(f" Removing {len(high_notes)} spurious high notes")
|
| 265 |
+
return [n for n in notes if n.pitch <= threshold_pitch]
|
| 266 |
+
|
| 267 |
+
return notes
|
| 268 |
+
|
| 269 |
+
def _smooth_timing(
|
| 270 |
+
self,
|
| 271 |
+
notes: List[note_seq.NoteSequence.Note],
|
| 272 |
+
quantize_ms: float = 50
|
| 273 |
+
) -> List[note_seq.NoteSequence.Note]:
|
| 274 |
+
"""
|
| 275 |
+
Quantize timing to remove jitter from AI artifacts.
|
| 276 |
+
|
| 277 |
+
Suno audio sometimes has unstable transients.
|
| 278 |
+
"""
|
| 279 |
+
quantize_sec = quantize_ms / 1000.0
|
| 280 |
+
|
| 281 |
+
for note in notes:
|
| 282 |
+
# Round to nearest quantize interval
|
| 283 |
+
note.start_time = round(note.start_time / quantize_sec) * quantize_sec
|
| 284 |
+
note.end_time = round(note.end_time / quantize_sec) * quantize_sec
|
| 285 |
+
|
| 286 |
+
return notes
|
| 287 |
+
|
| 288 |
+
|
| 289 |
+
# Convenience function for integration with main pipeline
|
| 290 |
+
def process_suno_audio(
|
| 291 |
+
audio_path: str,
|
| 292 |
+
output_path: str = None
|
| 293 |
+
) -> Tuple[str, bool, dict]:
|
| 294 |
+
"""
|
| 295 |
+
Detect and preprocess Suno audio in one call.
|
| 296 |
+
|
| 297 |
+
Args:
|
| 298 |
+
audio_path: Input audio
|
| 299 |
+
output_path: Output preprocessed audio (auto-generated if None)
|
| 300 |
+
|
| 301 |
+
Returns:
|
| 302 |
+
(processed_audio_path, is_suno, metrics)
|
| 303 |
+
"""
|
| 304 |
+
detector = SunoArtifactDetector()
|
| 305 |
+
is_suno, metrics = detector.analyze(audio_path)
|
| 306 |
+
|
| 307 |
+
if is_suno:
|
| 308 |
+
preprocessor = SunoAudioPreprocessor()
|
| 309 |
+
if output_path is None:
|
| 310 |
+
import os
|
| 311 |
+
base, ext = os.path.splitext(audio_path)
|
| 312 |
+
output_path = f"{base}_processed{ext}"
|
| 313 |
+
processed_path = preprocessor.process(audio_path, output_path)
|
| 314 |
+
return processed_path, is_suno, metrics
|
| 315 |
+
else:
|
| 316 |
+
# No processing needed
|
| 317 |
+
return audio_path, is_suno, metrics
|