Tab Agent Bot commited on
Commit
0f05314
·
1 Parent(s): ad07e38

add suno_postprocessor.py

Browse files
Files changed (1) hide show
  1. 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