[Python] How do I use a python script to filter out unwanted audio spillage from audio...

Discussão em 'Python' iniciado por Stack, Agosto 4, 2025.

  1. Stack

    Stack Membro Participativo

    [​IMG]

    I created a transcription and audio extraction python script using claude ai and whisper, it works well but the problem is I get these little audio spillage that comes into the audio extracted (see the little wave form at the start and end of the screenshot), how do I create a script to help eliminate these audio spillage? Code below.

    I tried creating an audio detector that detects very little audio sounds and interprets them as unwanted audio spillage however that hasn't worked well, Here is the code https://github.com/igittit/audiospill.git . I am expecting some suggestions on how to create a script to help remove these little audio spillage

    import os
    import numpy as np
    from pydub import AudioSegment
    from pydub.silence import detect_nonsilent

    def analyze_audio_levels(audio_file):
    """Analyze audio to suggest optimal thresholds"""
    audio = AudioSegment.from_file(audio_file)

    # Convert to numpy array for analysis
    samples = np.array(audio.get_array_of_samples())
    if audio.channels == 2:
    samples = samples.reshape((-1, 2))
    samples = samples.mean(axis=1)

    # Convert to float and normalize
    samples = samples.astype(np.float32)
    samples = samples / (2**15) # Normalize 16-bit audio

    # Calculate RMS in sliding windows
    window_size = int(audio.frame_rate * 0.1) # 100ms windows
    rms_values = []

    for i in range(0, len(samples) - window_size, window_size):
    window = samples[i:i + window_size]
    rms = np.sqrt(np.mean(window ** 2))
    if rms > 0:
    rms_db = 20 * np.log10(rms)
    rms_values.append(rms_db)

    if rms_values:
    # Suggest thresholds based on analysis
    noise_floor = np.percentile(rms_values, 20) # 20th percentile as noise floor
    suggested_threshold = noise_floor + 10 # 10dB above noise floor

    print(f"\n AUDIO ANALYSIS:")
    print(f" Noise floor: {noise_floor:.1f} dB")
    print(f" Suggested threshold: {suggested_threshold:.1f} dB")
    print(f" RMS range: {min(rms_values):.1f} to {max(rms_values):.1f} dB")

    return suggested_threshold

    return -40 # Default fallback

    def remove_silence_with_padding(audio_file, silence_thresh=-40, min_silence_len=500, min_sound_len=1000,
    use_adaptive=True, start_padding=100, end_padding=200,
    fade_in_duration=50, fade_out_duration=100):
    """
    Advanced silence removal with padding to preserve natural audio transitions

    Args:
    audio_file (str): Path to audio file
    silence_thresh (int): Threshold in dB
    min_silence_len (int): Minimum silence duration in ms
    min_sound_len (int): Minimum sound duration to keep in ms
    use_adaptive (bool): Use adaptive threshold based on audio analysis
    start_padding (int): Extra time to keep before audio starts (ms)
    end_padding (int): Extra time to keep after audio ends (ms)
    fade_in_duration (int): Fade in duration for smooth start (ms)
    fade_out_duration (int): Fade out duration for smooth end (ms)
    """
    print(f" Analyzing: {os.path.basename(audio_file)}")

    # Load audio
    audio = AudioSegment.from_file(audio_file)
    original_duration = len(audio) / 1000

    # Auto-detect better threshold if requested
    if use_adaptive:
    suggested_thresh = analyze_audio_levels(audio_file)
    if suggested_thresh > silence_thresh:
    print(f" Using adaptive threshold: {suggested_thresh:.1f} dB (instead of {silence_thresh})")
    silence_thresh = int(suggested_thresh)

    # Detect non-silent chunks
    nonsilent_chunks = detect_nonsilent(
    audio,
    min_silence_len=min_silence_len,
    silence_thresh=silence_thresh
    )

    if not nonsilent_chunks:
    print(f" ❌ No audio content detected")
    return None

    print(f" Found {len(nonsilent_chunks)} audio segments")

    # Filter chunks by duration and apply padding
    filtered_chunks = []
    total_removed_time = 0

    for i, (start_time, end_time) in enumerate(nonsilent_chunks):
    segment_duration = end_time - start_time

    # Extract segment for additional analysis
    segment = audio[start_time:end_time]

    # Calculate segment RMS
    samples = np.array(segment.get_array_of_samples(), dtype=np.float32)
    if segment.channels == 2:
    samples = samples.reshape((-1, 2)).mean(axis=1)

    rms = np.sqrt(np.mean((samples / 32768) ** 2))
    segment_db = 20 * np.log10(rms) if rms > 0 else -float('inf')

    # Apply duration and level filtering
    if segment_duration >= min_sound_len and segment_db > silence_thresh:
    # Apply padding to preserve natural transitions
    padded_start = max(0, start_time - start_padding)
    padded_end = min(len(audio), end_time + end_padding)

    filtered_chunks.append((padded_start, padded_end))
    print(f" ✓ Kept segment {i+1}: {start_time/1000:.1f}s-{end_time/1000:.1f}s → {padded_start/1000:.1f}s-{padded_end/1000:.1f}s ({segment_db:.1f}dB)")
    else:
    total_removed_time += segment_duration / 1000
    reason = "too short" if segment_duration < min_sound_len else "too quiet"
    print(f" ❌ Removed segment {i+1}: {start_time/1000:.1f}s-{end_time/1000:.1f}s ({reason}, {segment_db:.1f}dB)")

    if not filtered_chunks:
    print(f" ❌ No segments passed filtering")
    return None

    # Merge overlapping chunks (after padding, some might overlap)
    merged_chunks = []
    for start, end in sorted(filtered_chunks):
    if merged_chunks and start <= merged_chunks[-1][1]:
    # Overlapping or adjacent - merge
    merged_chunks[-1] = (merged_chunks[-1][0], max(merged_chunks[-1][1], end))
    else:
    merged_chunks.append((start, end))

    print(f" Merged into {len(merged_chunks)} continuous segments")

    # Combine all valid segments with smooth transitions
    cleaned_audio = AudioSegment.empty()

    for i, (start_time, end_time) in enumerate(merged_chunks):
    segment = audio[start_time:end_time]

    # Apply fade in/out for smooth transitions
    if i == 0 and fade_in_duration > 0:
    # Fade in at the very beginning
    segment = segment.fade_in(min(fade_in_duration, len(segment)))

    if i == len(merged_chunks) - 1 and fade_out_duration > 0:
    # Fade out at the very end
    segment = segment.fade_out(min(fade_out_duration, len(segment)))

    cleaned_audio += segment

    final_duration = len(cleaned_audio) / 1000
    time_saved = original_duration - final_duration

    print(f" Results:")
    print(f" Original: {original_duration:.1f}s")
    print(f" Cleaned: {final_duration:.1f}s")
    print(f" Removed: {time_saved:.1f}s ({time_saved/original_duration*100:.1f}%)")
    print(f" Applied padding: start +{start_padding}ms, end +{end_padding}ms")

    return cleaned_audio

    def process_folder_with_padding(input_folder, output_folder=None, **kwargs):
    """Process folder with padding settings"""

    if output_folder is None:
    output_folder = os.path.join(input_folder, "cleaned_audio_padded")

    os.makedirs(output_folder, exist_ok=True)

    audio_extensions = ['.mp3', '.wav', '.flac', '.m4a', '.ogg', '.wma']

    processed_count = 0
    skipped_count = 0

    for filename in os.listdir(input_folder):
    if any(filename.lower().endswith(ext) for ext in audio_extensions):
    input_path = os.path.join(input_folder, filename)

    try:
    cleaned_audio = remove_silence_with_padding(input_path, **kwargs)

    if cleaned_audio is not None:
    name, ext = os.path.splitext(filename)
    output_filename = f"{name}_cleaned{ext}"
    output_path = os.path.join(output_folder, output_filename)

    # Export with same format
    if ext.lower() == '.mp3':
    cleaned_audio.export(output_path, format="mp3", bitrate="192k")
    elif ext.lower() == '.flac':
    cleaned_audio.export(output_path, format="flac")
    else:
    cleaned_audio.export(output_path, format="wav")

    print(f" ✅ Saved: {output_filename}")
    processed_count += 1
    else:
    print(f" ⚠️ Skipped: {filename} (no content)")
    skipped_count += 1

    except Exception as e:
    print(f" ❌ Error processing {filename}: {str(e)}")
    skipped_count += 1
    else:
    print(f" ⚠️ Skipped: {filename} (unsupported format)")
    skipped_count += 1

    print(f"\n SUMMARY:")
    print(f" Processed: {processed_count} files")
    print(f" Skipped: {skipped_count} files")
    print(f" Output folder: {output_folder}")

    def main():
    print("=" * 70)
    print(" ENHANCED AUDIO SILENCE REMOVER WITH PADDING")
    print("=" * 70)
    print("This version preserves natural audio transitions and prevents abrupt cuts.")
    print()

    # Get input folder
    input_folder = input("Enter input folder path: ").strip().strip('"\'')
    if not os.path.exists(input_folder):
    print("❌ Folder does not exist!")
    return

    # Get output folder
    output_input = input(f"Enter output folder (Enter for default): ").strip().strip('"\'')
    output_folder = output_input if output_input else None

    print("\n️ ADVANCED SETTINGS:")
    print("For your waveform, try these settings:")
    print()

    # Get threshold settings
    print("Silence threshold recommendations:")
    print(" -45 dB: Aggressive (recommended for your case)")
    print(" -35 dB: Very aggressive")
    print(" -30 dB: Extremely aggressive")

    thresh_input = input("Silence threshold in dB (default: -35): ").strip()
    silence_thresh = -35 if not thresh_input else int(thresh_input)

    print("\nMinimum silence length:")
    print(" 300ms: Aggressive (recommended)")
    print(" 500ms: Moderate")

    silence_len_input = input("Min silence length in ms (default: 300): ").strip()
    min_silence_len = 300 if not silence_len_input else int(silence_len_input)

    print("\nMinimum sound length:")
    print(" 800ms: Keep substantial audio")
    print(" 1200ms: Very selective")

    sound_len_input = input("Min sound length in ms (default: 1000): ").strip()
    min_sound_len = 1000 if not sound_len_input else int(sound_len_input)

    # NEW: Padding settings
    print("\n️ PADDING SETTINGS (to prevent abrupt cuts):")

    print("Start padding (extra time before audio starts):")
    print(" 100ms: Conservative")
    print(" 200ms: Safe (recommended)")
    start_pad_input = input("Start padding in ms (default: 150): ").strip()
    start_padding = 150 if not start_pad_input else int(start_pad_input)

    print("\nEnd padding (extra time after audio ends):")
    print(" 200ms: Conservative")
    print(" 300ms: Safe (recommended)")
    print(" 500ms: Very safe")
    end_pad_input = input("End padding in ms (default: 300): ").strip()
    end_padding = 300 if not end_pad_input else int(end_pad_input)

    print("\nFade durations for smooth transitions:")
    fade_in_input = input("Fade in duration in ms (default: 50): ").strip()
    fade_in_duration = 50 if not fade_in_input else int(fade_in_input)

    fade_out_input = input("Fade out duration in ms (default: 100): ").strip()
    fade_out_duration = 100 if not fade_out_input else int(fade_out_input)

    adaptive_input = input("\nUse adaptive threshold detection? (Y/n): ").strip().lower()
    use_adaptive = adaptive_input != 'n'

    print(f"\n PROCESSING with settings:")
    print(f" Silence threshold: {silence_thresh} dB")
    print(f" Min silence length: {min_silence_len} ms")
    print(f" Min sound length: {min_sound_len} ms")
    print(f" Start padding: {start_padding} ms")
    print(f" End padding: {end_padding} ms")
    print(f" Fade in: {fade_in_duration} ms")
    print(f" Fade out: {fade_out_duration} ms")
    print(f" Adaptive detection: {use_adaptive}")
    print()

    confirm = input("Start processing? (y/N): ").strip().lower()
    if confirm == 'y':
    process_folder_with_padding(
    input_folder,
    output_folder,
    silence_thresh=silence_thresh,
    min_silence_len=min_silence_len,
    min_sound_len=min_sound_len,
    use_adaptive=use_adaptive,
    start_padding=start_padding,
    end_padding=end_padding,
    fade_in_duration=fade_in_duration,
    fade_out_duration=fade_out_duration
    )
    else:
    print("❌ Processing cancelled.")

    if __name__ == "__main__":
    main()

    Continue reading...

Compartilhe esta Página