Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "speech-prep"
version = "0.1.0"
dynamic = ["version"] # Tell build tools that version is determined dynamically
description = "Audio preprocessing toolkit for speech-to-text applications using ffmpeg"
readme = "README.md"
requires-python = ">=3.9"
Expand Down Expand Up @@ -33,9 +33,13 @@ Repository = "https://github.com/dimdasci/speech-prep"
Issues = "https://github.com/dimdasci/speech-prep/issues"

[build-system]
requires = ["hatchling"]
requires = ["hatchling", "hatch-vcs"]
build-backend = "hatchling.build"

[tool.hatch.version]
source = "vcs"
path = "src/speech_prep/_version.py"

[tool.ruff]
target-version = "py39"
line-length = 88
Expand Down Expand Up @@ -107,4 +111,5 @@ dev = [
"pytest-mock>=3.11.1",
"pydub>=0.25.1",
"pre-commit>=4.2.0",
"hatch-vcs>=0.5.0",
]
5 changes: 5 additions & 0 deletions scripts/dev.sh
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ function usage() {
echo " fix - Fix linting issues automatically"
echo " check-all - Run all checks"
echo " tests - Run tests"
echo " hooks - Run all pre-commit hooks on all files"
echo ""
exit 1
}
Expand Down Expand Up @@ -51,6 +52,10 @@ case "$1" in
echo "🧪 Running tests..."
uv run pytest tests/
;;
hooks)
echo "🪝 Running all pre-commit hooks on all files..."
uv run pre-commit run --all-files
;;
*)
usage
;;
Expand Down
4 changes: 2 additions & 2 deletions src/speech_prep/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
including silence detection and removal, speed adjustment, and format conversion.
"""

from ._version import __version__
from .core import SoundFile
from .exceptions import (
AudioPropertiesError,
Expand All @@ -14,13 +15,12 @@
SpeechPrepError,
)

__version__ = "0.1.0"

__all__ = [
"SoundFile",
"SpeechPrepError",
"FFmpegError",
"FileValidationError",
"AudioPropertiesError",
"SilenceDetectionError",
"__version__",
]
4 changes: 4 additions & 0 deletions src/speech_prep/_version.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
"""Version information."""

# This file is automatically generated by hatch-vcs
__version__ = "0.0.0" # This will be replaced during build
4 changes: 1 addition & 3 deletions src/speech_prep/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,7 @@ def __init__(
raise FileNotFoundError(f"Audio file not found: {self.path}")

try:
duration, self.format, file_size = get_audio_properties(self.path)
self.duration = float(duration)
self.file_size = int(file_size)
self.duration, self.file_size, self.format = get_audio_properties(self.path)
except Exception as e:
raise SpeechPrepError(f"Failed to extract metadata: {e}") from e

Expand Down
1 change: 1 addition & 0 deletions tests/integration/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Integration tests for speech-prep package."""
127 changes: 127 additions & 0 deletions tests/integration/test_sound_file_integration.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
"""Integration tests for SoundFile class using real files and no mocks."""

import shutil

import pytest

from speech_prep.core import SoundFile
from speech_prep.exceptions import SpeechPrepError

# Skip the entire module if ffmpeg is not installed
pytestmark = pytest.mark.skipif(
shutil.which("ffmpeg") is None, reason="ffmpeg is required for integration tests"
)


@pytest.fixture
def check_audio_file_exists(audio_file_with_silence):
"""Ensure the test audio file exists before running tests."""
file_path, _ = audio_file_with_silence
if not file_path.exists():
pytest.skip(f"Test audio file {file_path} not found")
return file_path


class TestSoundFileIntegrationWithRealFiles:
"""
Integration tests for SoundFile class using real files and no mocks.

These tests verify that the SoundFile class works correctly with real audio files
and the actual implementations of its dependencies.
"""

def test_soundfile_initialization(self, check_audio_file_exists):
"""Test SoundFile initialization with a real audio file."""
# Get the real audio file
file_path = check_audio_file_exists

# Create a SoundFile instance without any mocks
sound_file = SoundFile(file_path)

# Verify basic properties
assert sound_file.path == file_path
assert sound_file.duration > 0
assert isinstance(sound_file.format, str)
assert sound_file.file_size > 0

# Verify silence detection
assert isinstance(sound_file.silence_periods, list)

# Print debug info
print(f"File: {file_path}")
print(f"Duration: {sound_file.duration}")
print(f"Format: {sound_file.format}")
print(f"File size: {sound_file.file_size}")
print(f"Silence periods: {len(sound_file.silence_periods)}")

def test_full_processing_pipeline(self, check_audio_file_exists, temp_dir):
"""
Test a complete processing pipeline with real operations.

This test performs:
1. SoundFile initialization
2. Silence stripping
3. Speed adjustment
4. Format conversion

No functions are mocked, so this is a true integration test.
"""
# Get the real audio file
input_path = check_audio_file_exists

# Define paths for outputs
stripped_path = temp_dir / "real_stripped.wav"
sped_path = temp_dir / "real_sped.wav"
converted_path = temp_dir / "real_final.mp3"

# Create initial SoundFile
original = SoundFile(input_path)
print(f"Original file: {original}")

# 1. Strip silence
stripped = original.strip(stripped_path)
assert stripped is not None, "Strip operation failed"
assert stripped.path.exists(), "Stripped file doesn't exist"
assert (
stripped.duration <= original.duration
), "Stripped file should be shorter or equal"
print(f"Stripped file: {stripped}")

# 2. Adjust speed
speed_factor = 1.5
sped = stripped.speed(sped_path, speed_factor)
assert sped is not None, "Speed operation failed"
assert sped.path.exists(), "Sped file doesn't exist"
# Duration should be approximately 1/speed_factor of the original
# Allow for some tolerance due to encoding differences
expected_duration = stripped.duration / speed_factor
tolerance = 0.2 # 20% tolerance
assert (
abs(sped.duration - expected_duration) <= expected_duration * tolerance
), f"Speed adjustment incorrect: {sped.duration} vs {expected_duration}"
print(f"Sped file: {sped}")

# 3. Convert format
converted = sped.convert(converted_path, audio_bitrate="192k")
assert converted is not None, "Convert operation failed"
assert converted.path.exists(), "Converted file doesn't exist"
assert converted.format.lower() == "mp3", "Format conversion failed"
print(f"Converted file: {converted}")

# Verify final file properties
assert converted.duration > 0, "Final file has no duration"
assert converted.file_size > 0, "Final file has no size"

def test_error_handling_with_real_files(self, temp_dir):
"""Test error handling with real but problematic files."""
# Create an empty file (which will cause processing errors)
empty_file = temp_dir / "empty.wav"
empty_file.touch()

# Attempt to create a SoundFile from the empty file
with pytest.raises(SpeechPrepError) as exc_info:
SoundFile(empty_file)

assert "Failed to extract metadata" in str(
exc_info.value
), "Empty file should raise metadata extraction error"
40 changes: 20 additions & 20 deletions tests/test_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ def test_init_silence_detection_error(
# Mock get_audio_properties to return test values
mocker.patch(
"speech_prep.core.get_audio_properties",
return_value=(5.0, "wav", 10000),
return_value=(5.0, 10000, "wav"),
)

# Should not raise an exception, but should set silence_periods to empty list
Expand All @@ -74,7 +74,7 @@ def test_equality_same_path(
# Mock dependencies to ensure consistent behavior
mocker.patch(
"speech_prep.core.get_audio_properties",
return_value=(5.0, "wav", 10000),
return_value=(5.0, 10000, "wav"),
)
mocker.patch(
"speech_prep.core.detect_silence",
Expand All @@ -97,7 +97,7 @@ def test_equality_different_path(self, mocker: MockFixture, temp_dir: Path) -> N
# Mock dependencies to ensure consistent behavior
mocker.patch(
"speech_prep.core.get_audio_properties",
return_value=(5.0, "wav", 10000),
return_value=(5.0, 10000, "wav"),
)
mocker.patch(
"speech_prep.core.detect_silence",
Expand All @@ -118,7 +118,7 @@ def test_equality_non_soundfile(
# Mock dependencies to ensure consistent behavior
mocker.patch(
"speech_prep.core.get_audio_properties",
return_value=(5.0, "wav", 10000),
return_value=(5.0, 10000, "wav"),
)
mocker.patch(
"speech_prep.core.detect_silence",
Expand All @@ -145,7 +145,7 @@ def test_str_basic_info(
# Mock dependencies to return consistent values
mocker.patch(
"speech_prep.core.get_audio_properties",
return_value=(duration, "wav", 10000),
return_value=(duration, 10000, "wav"),
)
mocker.patch(
"speech_prep.core.detect_silence",
Expand All @@ -172,7 +172,7 @@ def test_str_no_silence_periods(
# Mock dependencies
mocker.patch(
"speech_prep.core.get_audio_properties",
return_value=(duration, "wav", 10000),
return_value=(duration, 10000, "wav"),
)
mocker.patch(
"speech_prep.core.detect_silence",
Expand Down Expand Up @@ -200,7 +200,7 @@ def test_str_few_silence_periods(
# Mock dependencies
mocker.patch(
"speech_prep.core.get_audio_properties",
return_value=(duration, "wav", 10000),
return_value=(duration, 10000, "wav"),
)
mocker.patch(
"speech_prep.core.detect_silence",
Expand Down Expand Up @@ -231,7 +231,7 @@ def test_str_many_silence_periods(
# Mock dependencies
mocker.patch(
"speech_prep.core.get_audio_properties",
return_value=(duration, "wav", 10000),
return_value=(duration, 10000, "wav"),
)
mocker.patch(
"speech_prep.core.detect_silence",
Expand Down Expand Up @@ -278,7 +278,7 @@ def test_strip_success(
# Mock dependencies for the input file
mocker.patch(
"speech_prep.core.get_audio_properties",
side_effect=[(duration, "wav", 10000), (3.0, "wav", 8000)],
side_effect=[(duration, 10000, "wav"), (3.0, 8000, "wav")],
)
mocker.patch(
"speech_prep.core.detect_silence",
Expand Down Expand Up @@ -327,7 +327,7 @@ def test_strip_no_silence(
# Mock dependencies
mocker.patch(
"speech_prep.core.get_audio_properties",
return_value=(duration, "wav", 10000),
return_value=(duration, 10000, "wav"),
)
mocker.patch(
"speech_prep.core.detect_silence",
Expand Down Expand Up @@ -362,7 +362,7 @@ def test_strip_error(
# Mock dependencies
mocker.patch(
"speech_prep.core.get_audio_properties",
return_value=(duration, "wav", 10000),
return_value=(duration, 10000, "wav"),
)
mocker.patch(
"speech_prep.core.detect_silence",
Expand Down Expand Up @@ -411,7 +411,7 @@ def test_convert_success(
# Mock dependencies for the input file
mocker.patch(
"speech_prep.core.get_audio_properties",
side_effect=[(duration, "wav", 10000), (duration, "mp3", 8000)],
side_effect=[(duration, 10000, "wav"), (duration, 8000, "mp3")],
)
mocker.patch(
"speech_prep.core.detect_silence",
Expand Down Expand Up @@ -454,7 +454,7 @@ def test_convert_error(
# Mock dependencies
mocker.patch(
"speech_prep.core.get_audio_properties",
return_value=(duration, "wav", 10000),
return_value=(duration, 10000, "wav"),
)
mocker.patch(
"speech_prep.core.detect_silence",
Expand Down Expand Up @@ -501,8 +501,8 @@ def test_speed_success(
mocker.patch(
"speech_prep.core.get_audio_properties",
side_effect=[
(duration, "wav", 10000),
(duration / speed_factor, "wav", 8000),
(duration, 10000, "wav"),
(duration / speed_factor, 8000, "wav"),
],
)
mocker.patch(
Expand Down Expand Up @@ -550,7 +550,7 @@ def test_speed_error(
# Mock dependencies
mocker.patch(
"speech_prep.core.get_audio_properties",
return_value=(duration, "wav", 10000),
return_value=(duration, 10000, "wav"),
)
mocker.patch(
"speech_prep.core.detect_silence",
Expand Down Expand Up @@ -610,13 +610,13 @@ def mock_process_file(*args, **kwargs):
# Mock get_audio_properties to return different values for different files
def mock_get_properties(file_path):
if str(file_path).endswith("stripped.wav"):
return 3.0, "wav", 8000
return 3.0, 8000, "wav"
elif str(file_path).endswith("sped.wav"):
return 2.0, "wav", 7000
return 2.0, 7000, "wav"
elif str(file_path).endswith("final.mp3"):
return 2.0, "mp3", 6000
return 2.0, 6000, "mp3"
else:
return 5.0, "wav", 10000
return 5.0, 10000, "wav"

mocker.patch(
"speech_prep.core.get_audio_properties", side_effect=mock_get_properties
Expand Down
Loading