Files
disbord/tests/integration/test_service_audio_integration.py
Travis Vasceannie 3acb779569 chore: remove .env.example and add new files for project structure
- Deleted .env.example file as it is no longer needed.
- Added .gitignore to manage ignored files and directories.
- Introduced CLAUDE.md for AI provider integration documentation.
- Created dev.sh for development setup and scripts.
- Updated Dockerfile and Dockerfile.production for improved build processes.
- Added multiple test files and directories for comprehensive testing.
- Introduced new utility and service files for enhanced functionality.
- Organized codebase with new directories and files for better maintainability.
2025-08-27 23:00:19 -04:00

425 lines
15 KiB
Python

"""
Service integration tests for Audio Services.
Tests the integration between audio recording, transcription, TTS,
laughter detection, and speaker diarization services with external dependencies.
"""
import asyncio
import os
import tempfile
import wave
from unittest.mock import AsyncMock, MagicMock
import numpy as np
import pytest
from core.ai_manager import AIProviderManager
from core.consent_manager import ConsentManager
from core.database import DatabaseManager
from services.audio.audio_recorder import AudioRecorderService
from services.audio.laughter_detection import LaughterDetector
from services.audio.speaker_recognition import SpeakerRecognitionService
from services.audio.transcription_service import TranscriptionService
from services.audio.tts_service import TTSService
@pytest.mark.integration
class TestAudioServiceIntegration:
"""Integration tests for audio service pipeline."""
@pytest.fixture
async def mock_dependencies(self):
"""Create all mock dependencies for audio services."""
return {
"ai_manager": self._create_mock_ai_manager(),
"db_manager": self._create_mock_db_manager(),
"consent_manager": self._create_mock_consent_manager(),
"settings": self._create_mock_settings(),
"audio_processor": self._create_mock_audio_processor(),
}
@pytest.fixture
async def audio_services(self, mock_dependencies):
"""Create integrated audio service instances."""
deps = mock_dependencies
# Create services with proper dependency injection
recorder = AudioRecorderService(
deps["settings"],
deps["consent_manager"],
None, # Speaker diarization is stubbed
)
transcription = TranscriptionService(
deps["ai_manager"],
deps["db_manager"],
None, # Speaker diarization is stubbed
deps["audio_processor"],
)
laughter = LaughterDetector(deps["settings"])
tts = TTSService(deps["ai_manager"], deps["settings"])
recognition = SpeakerRecognitionService(deps["db_manager"], deps["settings"])
# Initialize services
await transcription.initialize()
await laughter.initialize()
await tts.initialize()
await recognition.initialize()
return {
"recorder": recorder,
"transcription": transcription,
"laughter": laughter,
"tts": tts,
"recognition": recognition,
}
@pytest.fixture
def sample_audio_data(self):
"""Generate sample audio data for testing."""
sample_rate = 48000
duration = 10
# Generate sine wave audio
t = np.linspace(0, duration, sample_rate * duration)
audio_data = np.sin(2 * np.pi * 440 * t).astype(np.float32)
return {"audio": audio_data, "sample_rate": sample_rate, "duration": duration}
@pytest.fixture
def test_audio_file(self, sample_audio_data):
"""Create temporary audio file for testing."""
with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as f:
with wave.open(f.name, "wb") as wav_file:
wav_file.setnchannels(1)
wav_file.setsampwidth(2)
wav_file.setframerate(sample_audio_data["sample_rate"])
audio_int = (sample_audio_data["audio"] * 32767).astype(np.int16)
wav_file.writeframes(audio_int.tobytes())
yield f.name
# Cleanup
if os.path.exists(f.name):
os.unlink(f.name)
@pytest.mark.asyncio
async def test_audio_recording_to_transcription_pipeline(
self, audio_services, mock_dependencies, test_audio_file
):
"""Test full pipeline from recording to transcription."""
recorder = audio_services["recorder"]
transcription = audio_services["transcription"]
# Mock voice client
voice_client = MagicMock()
voice_client.is_connected.return_value = True
voice_client.channel.id = 123456
voice_client.channel.guild.id = 789012
# Start recording
success = await recorder.start_recording(voice_client, 123456, 789012)
assert success is True
assert 123456 in recorder.active_recordings
# Stop recording and get audio clip
audio_clip = await recorder.stop_recording(123456)
assert audio_clip is not None
# Transcribe the audio clip (with stubbed diarization)
transcription_result = await transcription.transcribe_audio_clip(
test_audio_file, 789012, 123456
)
assert transcription_result is not None
assert len(transcription_result.transcribed_segments) > 0
assert transcription_result.total_words > 0
# Verify AI manager was called for transcription
mock_dependencies["ai_manager"].transcribe.assert_called()
@pytest.mark.asyncio
async def test_laughter_detection_integration(
self, audio_services, test_audio_file
):
"""Test laughter detection integration with audio processing."""
laughter_detector = audio_services["laughter"]
# Mock participants for context
participants = [111, 222, 333]
# Detect laughter in audio
laughter_result = await laughter_detector.detect_laughter(
test_audio_file, participants
)
assert laughter_result is not None
assert hasattr(laughter_result, "total_laughter_duration")
assert hasattr(laughter_result, "laughter_segments")
assert laughter_result.processing_successful is True
@pytest.mark.asyncio
async def test_tts_service_integration(self, audio_services, mock_dependencies):
"""Test TTS service integration with AI providers."""
tts_service = audio_services["tts"]
# Mock AI response
mock_dependencies["ai_manager"].generate_speech.return_value = (
b"mock_audio_data"
)
# Generate speech
audio_data = await tts_service.generate_speech(
text="This is a test message", voice="alloy", guild_id=123456
)
assert audio_data is not None
assert len(audio_data) > 0
# Verify AI manager was called
mock_dependencies["ai_manager"].generate_speech.assert_called_with(
"This is a test message", voice="alloy"
)
@pytest.mark.asyncio
async def test_speaker_recognition_integration(
self, audio_services, mock_dependencies, test_audio_file
):
"""Test speaker recognition integration with database."""
recognition = audio_services["recognition"]
# Mock database response for known user
mock_dependencies["db_manager"].fetch_one.return_value = {
"user_id": 111,
"voice_profile": b"mock_voice_profile",
"confidence_threshold": 0.8,
}
# Perform speaker recognition
recognition_result = await recognition.identify_speaker(
test_audio_file, guild_id=123456
)
assert recognition_result is not None
assert recognition_result.get("user_id") is not None
assert recognition_result.get("confidence") is not None
# Verify database query
mock_dependencies["db_manager"].fetch_one.assert_called()
@pytest.mark.asyncio
async def test_transcription_with_stubbed_diarization(
self, audio_services, test_audio_file
):
"""Test transcription service handles stubbed speaker diarization gracefully."""
transcription = audio_services["transcription"]
# Transcribe without diarization (diarization_result = None)
result = await transcription.transcribe_audio_clip(
test_audio_file, 123456, 789012, diarization_result=None
)
assert result is not None
assert len(result.transcribed_segments) > 0
# When diarization is stubbed, should transcribe as single segment
segment = result.transcribed_segments[0]
assert segment.speaker_label == "SPEAKER_UNKNOWN"
assert segment.start_time == 0.0
assert segment.confidence > 0.0
@pytest.mark.asyncio
async def test_audio_processing_error_handling(
self, audio_services, mock_dependencies
):
"""Test error handling across audio service integrations."""
transcription = audio_services["transcription"]
# Simulate AI service error
mock_dependencies["ai_manager"].transcribe.side_effect = Exception(
"AI service error"
)
# Should handle error gracefully
result = await transcription.transcribe_audio_clip(
"/nonexistent/file.wav", 123456, 789012
)
assert result is None # Graceful failure
@pytest.mark.asyncio
async def test_concurrent_audio_processing(self, audio_services, test_audio_file):
"""Test concurrent processing across multiple audio services."""
transcription = audio_services["transcription"]
laughter = audio_services["laughter"]
recognition = audio_services["recognition"]
# Process same audio file concurrently with different services
tasks = [
transcription.transcribe_audio_clip(test_audio_file, 123456, 789012),
laughter.detect_laughter(test_audio_file, [111, 222]),
recognition.identify_speaker(test_audio_file, 123456),
]
results = await asyncio.gather(*tasks, return_exceptions=True)
# All tasks should complete without cross-interference
assert len(results) == 3
assert not any(isinstance(r, Exception) for r in results)
@pytest.mark.asyncio
async def test_audio_service_health_checks(self, audio_services):
"""Test health check integration across all audio services."""
health_checks = await asyncio.gather(
audio_services["transcription"].check_health(),
audio_services["laughter"].check_health(),
audio_services["tts"].check_health(),
audio_services["recognition"].check_health(),
return_exceptions=True,
)
assert len(health_checks) == 4
assert all(isinstance(h, dict) for h in health_checks)
assert all(
h.get("initialized") is True for h in health_checks if isinstance(h, dict)
)
@pytest.mark.asyncio
async def test_audio_quality_preservation_pipeline(
self, audio_services, sample_audio_data
):
"""Test audio quality preservation through processing pipeline."""
recorder = audio_services["recorder"]
# Process high-quality audio through pipeline
original_audio = sample_audio_data["audio"]
sample_rate = sample_audio_data["sample_rate"]
# Test audio quality preservation
processed_audio = await recorder.process_audio_stream(
original_audio, sample_rate
)
assert len(processed_audio) == len(original_audio)
# Allow 1% tolerance for processing artifacts
assert np.allclose(processed_audio, original_audio, rtol=0.01)
@pytest.mark.asyncio
async def test_consent_integration_with_audio_services(
self, audio_services, mock_dependencies
):
"""Test consent management integration across audio services."""
recorder = audio_services["recorder"]
consent_manager = mock_dependencies["consent_manager"]
# Set up consent scenarios
consent_manager.has_consent.return_value = True
consent_manager.get_consented_users.return_value = [111, 222]
# Mock voice client
voice_client = MagicMock()
voice_client.is_connected.return_value = True
voice_client.channel.id = 123456
voice_client.channel.guild.id = 789012
# Start recording - should check consent
success = await recorder.start_recording(voice_client, 123456, 789012)
assert success is True
# Verify consent was checked
consent_manager.has_consent.assert_called()
@pytest.mark.asyncio
async def test_audio_service_cleanup_integration(self, audio_services):
"""Test proper cleanup across all audio services."""
# Close all services
cleanup_tasks = [
audio_services["transcription"].close(),
audio_services["laughter"].close(),
audio_services["tts"].close(),
audio_services["recognition"].close(),
]
# Should complete without errors
await asyncio.gather(*cleanup_tasks, return_exceptions=True)
# Services should be properly cleaned up
assert not audio_services["transcription"]._initialized
def _create_mock_ai_manager(self) -> AsyncMock:
"""Create mock AI manager."""
ai_manager = AsyncMock(spec=AIProviderManager)
# Mock transcription response
transcription_result = MagicMock()
transcription_result.text = "This is test transcription"
transcription_result.confidence = 0.95
transcription_result.language = "en"
transcription_result.provider = "openai"
transcription_result.model = "whisper-1"
ai_manager.transcribe.return_value = transcription_result
# Mock speech generation
ai_manager.generate_speech.return_value = b"mock_audio_data"
# Mock health check
ai_manager.check_health.return_value = {"healthy": True}
return ai_manager
def _create_mock_db_manager(self) -> AsyncMock:
"""Create mock database manager."""
db_manager = AsyncMock(spec=DatabaseManager)
# Mock common database operations
db_manager.execute_query.return_value = True
db_manager.fetch_one.return_value = None
db_manager.fetch_all.return_value = []
return db_manager
def _create_mock_consent_manager(self) -> AsyncMock:
"""Create mock consent manager."""
consent_manager = AsyncMock(spec=ConsentManager)
consent_manager.has_consent.return_value = True
consent_manager.get_consented_users.return_value = [111, 222, 333]
consent_manager.check_channel_consent.return_value = True
return consent_manager
def _create_mock_settings(self) -> MagicMock:
"""Create mock settings."""
settings = MagicMock()
# Audio settings
settings.audio_buffer_duration = 120
settings.audio_sample_rate = 48000
settings.audio_channels = 2
settings.temp_audio_dir = "/tmp/audio"
settings.max_concurrent_recordings = 10
# TTS settings
settings.tts_default_voice = "alloy"
settings.tts_speed = 1.0
# Laughter detection settings
settings.laughter_min_duration = 0.5
settings.laughter_confidence_threshold = 0.7
return settings
def _create_mock_audio_processor(self) -> MagicMock:
"""Create mock audio processor."""
processor = MagicMock()
processor.get_audio_info.return_value = {
"duration": 10.0,
"sample_rate": 48000,
"channels": 1,
}
return processor