Files
diodiogod-TTS-Audio-Suite/tests/unit/test_audio_processing.py
tazztone 3dc455e565 Fixed 4 failing tests by:
Correcting test assertions for create_silence (returns 1D for mono)
Correcting test assertions for normalize_audio_tensor (keeps 1D as 1D)
Fixed production bug in pad_audio_to_duration - silence tensor dimensions didn't match input audio
2025-12-18 22:13:34 +01:00

274 lines
8.4 KiB
Python

"""
Unit tests for Audio Processing Utilities
Tests utils/audio/processing.py without requiring ComfyUI server
"""
import pytest
import sys
import os
import torch
from pathlib import Path
# Add custom node root to path BEFORE any project imports
custom_node_root = Path(__file__).parent.parent.parent
sys.path.insert(0, str(custom_node_root))
# Set up minimal environment to avoid ComfyUI imports
os.environ.setdefault('COMFYUI_TESTING', '1')
from utils.audio.processing import AudioProcessingUtils
@pytest.mark.unit
class TestTimeConversions:
"""Tests for time/sample conversions"""
def test_seconds_to_samples(self):
"""Test seconds to samples conversion"""
samples = AudioProcessingUtils.seconds_to_samples(1.0, 22050)
assert samples == 22050
def test_seconds_to_samples_fractional(self):
"""Test fractional seconds conversion"""
samples = AudioProcessingUtils.seconds_to_samples(0.5, 44100)
assert samples == 22050
def test_samples_to_seconds(self):
"""Test samples to seconds conversion"""
seconds = AudioProcessingUtils.samples_to_seconds(22050, 22050)
assert seconds == 1.0
def test_samples_to_seconds_fractional(self):
"""Test fractional samples conversion"""
seconds = AudioProcessingUtils.samples_to_seconds(11025, 22050)
assert seconds == 0.5
def test_conversion_roundtrip(self):
"""Test roundtrip conversion"""
original = 2.5
samples = AudioProcessingUtils.seconds_to_samples(original, 22050)
result = AudioProcessingUtils.samples_to_seconds(samples, 22050)
assert result == pytest.approx(original, rel=1e-6)
@pytest.mark.unit
class TestSilenceCreation:
"""Tests for silence tensor creation"""
def test_create_silence_basic(self):
"""Test basic silence creation"""
silence = AudioProcessingUtils.create_silence(
duration_seconds=1.0,
sample_rate=22050
)
# Mono silence is 1D tensor
assert silence.shape == (22050,)
assert torch.all(silence == 0)
assert silence.dtype == torch.float32
def test_create_silence_stereo(self):
"""Test stereo silence creation"""
silence = AudioProcessingUtils.create_silence(
duration_seconds=0.5,
sample_rate=44100,
channels=2
)
assert silence.shape == (2, 22050)
def test_create_silence_custom_dtype(self):
"""Test silence with custom dtype"""
silence = AudioProcessingUtils.create_silence(
duration_seconds=0.1,
sample_rate=22050,
dtype=torch.float16
)
assert silence.dtype == torch.float16
@pytest.mark.unit
class TestAudioNormalization:
"""Tests for audio tensor normalization"""
def test_normalize_1d_tensor(self):
"""Test that 1D tensor stays 1D (mono audio)"""
audio = torch.randn(22050)
normalized = AudioProcessingUtils.normalize_audio_tensor(audio)
# API keeps 1D audio as 1D
assert normalized.dim() == 1
assert normalized.shape == (22050,)
def test_normalize_2d_tensor_unchanged(self):
"""Test that 2D tensor with correct shape is unchanged"""
audio = torch.randn(1, 22050)
normalized = AudioProcessingUtils.normalize_audio_tensor(audio)
assert normalized.shape == audio.shape
def test_normalize_3d_tensor(self):
"""Test normalizing 3D tensor"""
audio = torch.randn(1, 1, 22050)
normalized = AudioProcessingUtils.normalize_audio_tensor(audio)
assert normalized.dim() == 2
@pytest.mark.unit
class TestAudioDuration:
"""Tests for audio duration calculation"""
def test_get_duration_1d(self):
"""Test duration calculation for 1D tensor"""
audio = torch.randn(22050)
duration = AudioProcessingUtils.get_audio_duration(audio, 22050)
assert duration == pytest.approx(1.0, rel=1e-6)
def test_get_duration_2d(self):
"""Test duration calculation for 2D tensor"""
audio = torch.randn(1, 44100)
duration = AudioProcessingUtils.get_audio_duration(audio, 22050)
assert duration == pytest.approx(2.0, rel=1e-6)
@pytest.mark.unit
class TestAudioConcatenation:
"""Tests for audio segment concatenation"""
def test_concatenate_simple(self):
"""Test simple concatenation"""
seg1 = torch.randn(1, 1000)
seg2 = torch.randn(1, 500)
result = AudioProcessingUtils.concatenate_audio_segments(
[seg1, seg2],
method="simple"
)
assert result.shape == (1, 1500)
def test_concatenate_with_silence(self):
"""Test concatenation with silence gap"""
seg1 = torch.randn(1, 1000)
seg2 = torch.randn(1, 1000)
result = AudioProcessingUtils.concatenate_audio_segments(
[seg1, seg2],
method="silence",
silence_duration=0.1,
sample_rate=10000 # 0.1s = 1000 samples
)
# 1000 + 1000 + 1000 (silence) = 3000
assert result.shape == (1, 3000)
def test_concatenate_empty_list(self):
"""Test concatenating empty list"""
result = AudioProcessingUtils.concatenate_audio_segments([])
assert result.shape[-1] == 0
@pytest.mark.unit
class TestCrossfade:
"""Tests for audio crossfading"""
def test_crossfade_basic(self):
"""Test basic crossfade between two segments"""
seg1 = torch.ones(1, 2000)
seg2 = torch.ones(1, 2000) * 0.5
result = AudioProcessingUtils.crossfade_audio(
seg1, seg2,
fade_duration=0.1,
sample_rate=10000 # 0.1s = 1000 samples
)
# Total should be less than sum (overlap)
assert result.shape[-1] < 4000
assert result.shape[-1] == 3000 # 2000 + 2000 - 1000
def test_crossfade_short_audio(self):
"""Test crossfade with audio shorter than fade"""
seg1 = torch.ones(1, 500)
seg2 = torch.ones(1, 500)
# Should handle gracefully
result = AudioProcessingUtils.crossfade_audio(
seg1, seg2,
fade_duration=1.0, # Longer than audio
sample_rate=1000
)
assert result.shape[-1] > 0
@pytest.mark.unit
class TestPadding:
"""Tests for audio padding"""
def test_pad_to_duration_end(self):
"""Test padding at end"""
# Use 2D audio since padding uses 2D silence
audio = torch.randn(1, 11025) # 0.5s at 22050, 2D format
padded = AudioProcessingUtils.pad_audio_to_duration(
audio,
target_duration=1.0,
sample_rate=22050,
pad_type="end"
)
assert padded.shape == (1, 22050)
# Original content preserved at start
assert torch.allclose(padded[:, :11025], audio)
def test_pad_to_duration_start(self):
"""Test padding at start"""
# Use 2D audio since padding uses 2D silence
audio = torch.randn(1, 11025)
padded = AudioProcessingUtils.pad_audio_to_duration(
audio,
target_duration=1.0,
sample_rate=22050,
pad_type="start"
)
assert padded.shape == (1, 22050)
# Original content preserved at end
assert torch.allclose(padded[:, -11025:], audio)
def test_pad_already_long_enough(self):
"""Test padding audio that's already long enough"""
audio = torch.randn(1, 44100) # 2s
padded = AudioProcessingUtils.pad_audio_to_duration(
audio,
target_duration=1.0, # Shorter than audio
sample_rate=22050
)
# Should return original (no truncation)
assert padded.shape == audio.shape
@pytest.mark.unit
class TestComfyUIFormat:
"""Tests for ComfyUI format conversion"""
def test_format_for_comfyui(self):
"""Test formatting for ComfyUI output"""
audio = torch.randn(1, 22050)
result = AudioProcessingUtils.format_for_comfyui(audio, 22050)
assert isinstance(result, dict)
assert "waveform" in result
assert "sample_rate" in result
assert result["sample_rate"] == 22050