clustaudio 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. clustaudio/__init__.py +87 -0
  2. clustaudio/atc_example.py +378 -0
  3. clustaudio/clustaudio.py +2915 -0
  4. clustaudio/examples.py +779 -0
  5. clustaudio/fingerprint.py +353 -0
  6. clustaudio/metadata.py +364 -0
  7. clustaudio/separation.py +617 -0
  8. clustaudio/tests/audio/atc/ACCU-0agmXf.wav +0 -0
  9. clustaudio/tests/audio/atc/ACCU-1DTI8r.wav +0 -0
  10. clustaudio/tests/audio/atc/ACCU-1ZvQqb.wav +0 -0
  11. clustaudio/tests/audio/atc/ACCU-2nUNxX.wav +0 -0
  12. clustaudio/tests/audio/atc/ACCU-3qIKv5.wav +0 -0
  13. clustaudio/tests/audio/atc/ACCU-4BTi3A.wav +0 -0
  14. clustaudio/tests/audio/atc/ACCU-4U0PXR.wav +0 -0
  15. clustaudio/tests/audio/atc/ACCU-5n3Cmr.wav +0 -0
  16. clustaudio/tests/audio/atc/ACCU-6ENnqU.wav +0 -0
  17. clustaudio/tests/audio/atc/ACCU-6y7b3L.wav +0 -0
  18. clustaudio/tests/audio/various/Gunshot_and_gunfire_1.wav +0 -0
  19. clustaudio/tests/audio/various/Gunshot_and_gunfire_2.wav +0 -0
  20. clustaudio/tests/audio/various/Gunshot_and_gunfire_3.wav +0 -0
  21. clustaudio/tests/audio/various/Knock_2.wav +0 -0
  22. clustaudio/tests/audio/various/Knock_3.wav +0 -0
  23. clustaudio/tests/audio/various/Knock_4.wav +0 -0
  24. clustaudio/tests/audio/various/Splash_and_splatter_1.wav +0 -0
  25. clustaudio/tests/audio/various/Splash_and_splatter_2.wav +0 -0
  26. clustaudio/tests/audio/various/Splash_and_splatter_3.wav +0 -0
  27. clustaudio/tests/audio/various/dog_barking_1.wav +0 -0
  28. clustaudio/tests/audio/various/dog_barking_2.wav +0 -0
  29. clustaudio/tests/audio/various/dog_barking_3.wav +0 -0
  30. clustaudio/tests/audio/various/drilling_1.wav +0 -0
  31. clustaudio/tests/audio/various/drilling_2.wav +0 -0
  32. clustaudio/tests/audio/various/drilling_3.wav +0 -0
  33. clustaudio/tests/audio/various/siren_1.wav +0 -0
  34. clustaudio/tests/audio/various/siren_2.wav +0 -0
  35. clustaudio/tests/audio/various/siren_3.wav +0 -0
  36. clustaudio/vad.py +380 -0
  37. clustaudio/visualization.py +592 -0
  38. clustaudio-0.1.0.dist-info/METADATA +607 -0
  39. clustaudio-0.1.0.dist-info/RECORD +42 -0
  40. clustaudio-0.1.0.dist-info/WHEEL +5 -0
  41. clustaudio-0.1.0.dist-info/licenses/LICENSE +22 -0
  42. clustaudio-0.1.0.dist-info/top_level.txt +1 -0
clustaudio/__init__.py ADDED
@@ -0,0 +1,87 @@
1
+ import logging
2
+
3
+ from clustaudio.clustaudio import Clustaudio
4
+
5
+ import clustaudio.vad as vad
6
+ import clustaudio.separation as separation
7
+ import clustaudio.visualization as visualization
8
+ import clustaudio.visualization as visualization
9
+ import clustaudio.fingerprint as fingerprint
10
+ import clustaudio.metadata as metadata
11
+
12
+ from clustaudio.clustaudio import (
13
+ import_example,
14
+ listdir,
15
+ set_logger,
16
+ move_files,
17
+ )
18
+
19
+
20
+ __author__ = 'Erdogan Tasksen'
21
+ __email__ = 'erdogant@gmail.com'
22
+ __version__ = '0.1.0'
23
+
24
+ # Setup root logger
25
+ _logger = logging.getLogger('clustaudio')
26
+ _log_handler = logging.StreamHandler()
27
+ _fmt = '[{asctime}] [{name}] [{levelname}] {msg}'
28
+ _formatter = logging.Formatter(fmt=_fmt, style='{', datefmt='%d-%m-%Y %H:%M:%S')
29
+ _log_handler.setFormatter(_formatter)
30
+ _log_handler.setLevel(logging.DEBUG)
31
+ _logger.addHandler(_log_handler)
32
+ _logger.propagate = False
33
+
34
+ # module level doc-string
35
+ __doc__ = """
36
+ clustaudio
37
+ =====================================================================
38
+
39
+ Python package clustaudio is to detect natural groups or clusters of images.
40
+
41
+ The aim of ``clustaudio`` is to detect natural groups or clusters of images. It works using a multi-step proces of carefully pre-processing the images, extracting the features, and evaluating the optimal number of clusters across the feature space.
42
+ The optimal number of clusters are determined using well known methods suchs as *silhouette, dbindex, and derivatives* in combination with clustering methods, such as *agglomerative, kmeans, dbscan and hdbscan*.
43
+ With ``clustaudio`` we aim to determine the most robust clustering by efficiently searching across the parameter and evaluation the clusters.
44
+ Besides clustering of images, the ``clustaudio`` model can also be used to find the most similar images for a new unseen sample.
45
+
46
+ Example
47
+ -------
48
+ >>> from clustaudio import clustaudio
49
+ >>>
50
+ >>> # Init with default settings
51
+ >>> cl = clustaudio(method='pca')
52
+ >>>
53
+ >>> # load example dataset
54
+ >>> X, y = cl.import_example(data='mnist')
55
+ >>>
56
+ >>> # Cluster digits
57
+ >>> results = cl.fit_transform(X)
58
+ >>>
59
+ >>> # Cluster evaluation
60
+ >>> cl.clustaudio.plot()
61
+ >>> cl.clustaudio.scatter(cl.results['xycoord'])
62
+ >>> cl.pca.plot()
63
+ >>>
64
+ >>> # Unique
65
+ >>> cl.plot_unique(img_mean=False)
66
+ >>> cl.results_unique.keys()
67
+ >>>
68
+ >>> # Scatter
69
+ >>> cl.scatter(img_mean=False, zoom=3)
70
+ >>>
71
+ >>> # Plot clustered images
72
+ >>> cl.plot(labels=8)
73
+ >>>
74
+ >>> # Plot dendrogram
75
+ >>> cl.dendrogram()
76
+ >>>
77
+ >>> # Find images
78
+ >>> results_find = cl.find(X[0,:], k=None, alpha=0.05)
79
+ >>> cl.plot_find()
80
+ >>> cl.scatter()
81
+ >>>
82
+
83
+ References
84
+ ----------
85
+ https://github.com/erdogant/clustaudio
86
+
87
+ """
@@ -0,0 +1,378 @@
1
+ """
2
+ Complete ATC Audio Clustering Example
3
+ ======================================
4
+
5
+ This script demonstrates the full workflow for processing noisy
6
+ Air Traffic Control communications with clustaudio.
7
+
8
+ Usage:
9
+ python atc_example.py --input ./ATC_recordings/ --output ./results/
10
+
11
+ Requirements:
12
+ pip install librosa soundfile numpy pandas scikit-learn scipy matplotlib tqdm
13
+ pip install pca distfit clustaudio scatterd colourmap datazets pypickle ismember
14
+ """
15
+
16
+ import os
17
+ import sys
18
+ import argparse
19
+ import numpy as np
20
+ import pandas as pd
21
+ from pathlib import Path
22
+
23
+ # Import clustaudio
24
+ from clustaudio import clustaudio, listdir
25
+ import vad
26
+ import separation
27
+ import visualization as viz
28
+
29
+
30
+ def process_atc_communications(input_dir, output_dir, enhancement_level='aggressive',
31
+ save_plots=True, verbose=True):
32
+ """
33
+ Process ATC communications with full pipeline.
34
+
35
+ Parameters
36
+ ----------
37
+ input_dir : str
38
+ Directory containing ATC audio files
39
+ output_dir : str
40
+ Directory for output files
41
+ enhancement_level : str
42
+ 'light', 'moderate', or 'aggressive'
43
+ save_plots : bool
44
+ Save visualization plots
45
+ verbose : bool
46
+ Print progress messages
47
+
48
+ Returns
49
+ -------
50
+ dict
51
+ Results dictionary with all processing outputs
52
+ """
53
+ # Create output directories
54
+ os.makedirs(output_dir, exist_ok=True)
55
+ os.makedirs(os.path.join(output_dir, 'enhanced'), exist_ok=True)
56
+ os.makedirs(os.path.join(output_dir, 'separated'), exist_ok=True)
57
+ os.makedirs(os.path.join(output_dir, 'segments_voice'), exist_ok=True)
58
+ os.makedirs(os.path.join(output_dir, 'plots'), exist_ok=True)
59
+ os.makedirs(os.path.join(output_dir, 'organized'), exist_ok=True)
60
+
61
+ if verbose:
62
+ print("=" * 70)
63
+ print("ATC Audio Clustering Pipeline")
64
+ print("=" * 70)
65
+ print(f"Input: {input_dir}")
66
+ print(f"Output: {output_dir}")
67
+ print(f"Enhancement: {enhancement_level}")
68
+ print("=" * 70)
69
+
70
+ # Initialize clustaudio
71
+ if verbose:
72
+ print("\n[1/8] Initializing clustaudio...")
73
+
74
+ cl = clustaudio(
75
+ method='mfcc',
76
+ sr=16000, # Typical for radio communications
77
+ mono=True,
78
+ normalize=True,
79
+ params_mfcc={
80
+ 'n_mfcc': 20,
81
+ 'n_fft': 2048,
82
+ 'hop_length': 512
83
+ },
84
+ verbose='info'
85
+ )
86
+
87
+ # Get audio files
88
+ if verbose:
89
+ print("\n[2/8] Scanning for audio files...")
90
+
91
+ filepaths = listdir(input_dir, ext=['wav', 'mp3', 'flac', 'ogg', 'm4a'])
92
+
93
+ if len(filepaths) == 0:
94
+ print(f"ERROR: No audio files found in {input_dir}")
95
+ return None
96
+
97
+ if verbose:
98
+ print(f"Found {len(filepaths)} audio files")
99
+
100
+ # Step 1: Enhance ATC audio
101
+ if verbose:
102
+ print(f"\n[3/8] Enhancing audio (method: {enhancement_level})...")
103
+
104
+ enhanced = cl.enhance_atc_audio(
105
+ filepaths,
106
+ method=enhancement_level,
107
+ output_dir=os.path.join(output_dir, 'enhanced')
108
+ )
109
+
110
+ n_enhanced = len([p for p in enhanced['pathnames_enhanced'] if p is not None])
111
+ if verbose:
112
+ print(f"Enhanced {n_enhanced}/{len(filepaths)} files successfully")
113
+
114
+ # Step 2: Separate foreground (voice) from background (noise)
115
+ if verbose:
116
+ print("\n[4/8] Separating voice from noise...")
117
+
118
+ separated = cl.separate_foreground_background(
119
+ enhanced['pathnames_enhanced'],
120
+ method='spectral_masking',
121
+ denoise=False, # Already enhanced
122
+ output_dir=os.path.join(output_dir, 'separated'),
123
+ save_both=True
124
+ )
125
+
126
+ avg_snr = np.mean([s for s in separated['snr_db'] if s is not None])
127
+ if verbose:
128
+ print(f"Separated {len(separated['pathnames_foreground'])} files")
129
+ print(f"Average SNR: {avg_snr:.2f} dB")
130
+
131
+ # Step 3: Extract voice segments
132
+ if verbose:
133
+ print("\n[5/8] Extracting voice segments...")
134
+
135
+ segments_voice = vad.batch_extract_voice_segments(
136
+ separated['pathnames_foreground'],
137
+ output_dir=os.path.join(output_dir, 'segments_voice'),
138
+ sr=16000,
139
+ method='energy',
140
+ energy_threshold=0.015,
141
+ min_silence_duration=0.3,
142
+ min_segment_duration=0.5
143
+ )
144
+
145
+ # Collect voice segment paths
146
+ pathnames_voice = [
147
+ p
148
+ for seg in segments_voice
149
+ if seg.get("pathnames_segments")
150
+ for p in seg["pathnames_segments"]
151
+ ]
152
+
153
+ if verbose:
154
+ print(f"Extracted {len(pathnames_voice)} voice segments from {len(segments_voice)} files")
155
+ avg_segments = len(pathnames_voice) / max(len(segments_voice), 1)
156
+ print(f"Average segments per file: {avg_segments:.1f}")
157
+
158
+ # Step 4: Cluster voice segments
159
+ if verbose:
160
+ print("\n[6/8] Clustering voice segments...")
161
+
162
+ if len(pathnames_voice) < 10:
163
+ print("WARNING: Too few voice segments for reliable clustering")
164
+ print(f"Found only {len(pathnames_voice)} segments (minimum 10 recommended)")
165
+ return {
166
+ 'enhanced': enhanced,
167
+ 'separated': separated,
168
+ 'segments_voice': segments_voice,
169
+ 'error': 'Too few segments'
170
+ }
171
+
172
+ results_voice = cl.fit_transform(pathnames_voice)
173
+
174
+ labels_voice = cl.cluster(
175
+ min_clust=max(3, min(10, len(pathnames_voice) // 20)),
176
+ max_clust=min(25, len(pathnames_voice) // 10),
177
+ cluster='agglomerative',
178
+ evaluate='silhouette',
179
+ metric='cosine',
180
+ cluster_space='high'
181
+ )
182
+
183
+ n_voice_clusters = len(np.unique(labels_voice))
184
+ if verbose:
185
+ print(f"Found {n_voice_clusters} voice clusters")
186
+ for i in range(n_voice_clusters):
187
+ count = np.sum(labels_voice == i)
188
+ print(f" Cluster {i}: {count} segments")
189
+
190
+ # Step 5: Cluster background noise (optional but useful)
191
+ if verbose:
192
+ print("\n[7/8] Clustering background noise...")
193
+
194
+ results_noise = cl.fit_transform(separated['pathnames_background'])
195
+
196
+ labels_noise = cl.cluster(
197
+ min_clust=2,
198
+ max_clust=8,
199
+ cluster='dbscan',
200
+ metric='euclidean'
201
+ )
202
+
203
+ n_noise_clusters = len(np.unique(labels_noise))
204
+ if verbose:
205
+ print(f"Found {n_noise_clusters} noise types")
206
+
207
+ # Step 6: Generate visualizations
208
+ if save_plots and verbose:
209
+ print("\n[8/8] Generating visualizations...")
210
+
211
+ plots_dir = os.path.join(output_dir, 'plots')
212
+
213
+ if save_plots:
214
+ # Plot cluster statistics
215
+ try:
216
+ fig = cl.plot_statistics()
217
+ if fig:
218
+ viz.save_figure(fig, os.path.join(plots_dir, 'cluster_statistics.png'))
219
+ viz.close_all()
220
+ except Exception as e:
221
+ print(f"Warning: Could not create statistics plot: {e}")
222
+
223
+ # Plot separation results (first 3 files)
224
+ for i in range(min(3, len(separated['results']))):
225
+ try:
226
+ fig = cl.plot_separation(index=i)
227
+ if fig:
228
+ viz.save_figure(fig, os.path.join(plots_dir, f'separation_{i}.png'))
229
+ viz.close_all()
230
+ except Exception as e:
231
+ print(f"Warning: Could not create separation plot {i}: {e}")
232
+
233
+ if verbose:
234
+ print(f"Saved plots to {plots_dir}")
235
+
236
+ # Create reports
237
+ if verbose:
238
+ print("\nGenerating reports...")
239
+
240
+ # Voice cluster report
241
+ voice_report = pd.DataFrame({
242
+ 'segment_path': pathnames_voice,
243
+ 'voice_cluster': labels_voice
244
+ })
245
+ voice_report.to_csv(os.path.join(output_dir, 'voice_clusters.csv'), index=False)
246
+
247
+ # Combined report
248
+ combined_report = pd.DataFrame({
249
+ 'original_file': [os.path.basename(r['pathname']) for r in separated['results']],
250
+ 'enhanced_file': [os.path.basename(p) for p in enhanced['pathnames_enhanced']],
251
+ 'foreground_file': [os.path.basename(p) if p else None for p in separated['pathnames_foreground']],
252
+ 'background_file': [os.path.basename(p) if p else None for p in separated['pathnames_background']],
253
+ 'snr_db': separated['snr_db'],
254
+ 'noise_cluster': labels_noise
255
+ })
256
+ combined_report.to_csv(os.path.join(output_dir, 'processing_report.csv'), index=False)
257
+
258
+ # Summary statistics
259
+ summary = {
260
+ 'total_files': len(filepaths),
261
+ 'enhanced_files': n_enhanced,
262
+ 'separated_files': len(separated['pathnames_foreground']),
263
+ 'voice_segments': len(pathnames_voice),
264
+ 'voice_clusters': n_voice_clusters,
265
+ 'noise_clusters': n_noise_clusters,
266
+ 'avg_snr_db': float(avg_snr),
267
+ 'avg_segments_per_file': float(len(pathnames_voice) / max(len(segments_voice), 1))
268
+ }
269
+
270
+ # Save summary
271
+ summary_df = pd.DataFrame([summary])
272
+ summary_df.to_csv(os.path.join(output_dir, 'summary.csv'), index=False)
273
+
274
+ # Save model
275
+ model_path = os.path.join(output_dir, 'atc_model.pkl')
276
+ cl.save(model_path)
277
+
278
+ if verbose:
279
+ print("\n" + "=" * 70)
280
+ print("PROCESSING COMPLETE!")
281
+ print("=" * 70)
282
+ print(f"Total files processed: {len(filepaths)}")
283
+ print(f"Voice segments extracted: {len(pathnames_voice)}")
284
+ print(f"Voice clusters found: {n_voice_clusters}")
285
+ print(f"Noise types found: {n_noise_clusters}")
286
+ print(f"Average SNR improvement: {avg_snr:.2f} dB")
287
+ print(f"\nResults saved to: {output_dir}")
288
+ print(f"Model saved to: {model_path}")
289
+ print("=" * 70)
290
+
291
+ return {
292
+ 'enhanced': enhanced,
293
+ 'separated': separated,
294
+ 'segments_voice': segments_voice,
295
+ 'pathnames_voice': pathnames_voice,
296
+ 'results_voice': results_voice,
297
+ 'labels_voice': labels_voice,
298
+ 'results_noise': results_noise,
299
+ 'labels_noise': labels_noise,
300
+ 'summary': summary,
301
+ 'model_path': model_path,
302
+ 'clustaudio': cl
303
+ }
304
+
305
+
306
+ def main():
307
+ """Main entry point for command-line usage."""
308
+ parser = argparse.ArgumentParser(
309
+ description='Process ATC communications with clustaudio',
310
+ formatter_class=argparse.RawDescriptionHelpFormatter,
311
+ epilog="""
312
+ Examples:
313
+ # Basic usage
314
+ python atc_example.py --input ./ATC_recordings/ --output ./results/
315
+
316
+ # With custom enhancement
317
+ python atc_example.py --input ./ATC/ --output ./out/ --enhancement aggressive
318
+
319
+ # Without plots
320
+ python atc_example.py --input ./ATC/ --output ./out/ --no-plots
321
+ """
322
+ )
323
+
324
+ parser.add_argument(
325
+ '--input', '-i',
326
+ required=True,
327
+ help='Input directory containing ATC audio files'
328
+ )
329
+
330
+ parser.add_argument(
331
+ '--output', '-o',
332
+ required=True,
333
+ help='Output directory for results'
334
+ )
335
+
336
+ parser.add_argument(
337
+ '--enhancement', '-e',
338
+ default='aggressive',
339
+ choices=['light', 'moderate', 'aggressive'],
340
+ help='Enhancement level (default: aggressive)'
341
+ )
342
+
343
+ parser.add_argument(
344
+ '--no-plots',
345
+ action='store_true',
346
+ help='Skip generating visualization plots'
347
+ )
348
+
349
+ parser.add_argument(
350
+ '--quiet', '-q',
351
+ action='store_true',
352
+ help='Minimal output'
353
+ )
354
+
355
+ args = parser.parse_args()
356
+
357
+ # Validate input directory
358
+ if not os.path.isdir(args.input):
359
+ print(f"ERROR: Input directory does not exist: {args.input}")
360
+ sys.exit(1)
361
+
362
+ # Process
363
+ results = process_atc_communications(
364
+ input_dir=args.input,
365
+ output_dir=args.output,
366
+ enhancement_level=args.enhancement,
367
+ save_plots=not args.no_plots,
368
+ verbose=not args.quiet
369
+ )
370
+
371
+ if results is None or results.get('error'):
372
+ sys.exit(1)
373
+
374
+ return 0
375
+
376
+
377
+ if __name__ == '__main__':
378
+ sys.exit(main())