- 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.
425 lines
15 KiB
Python
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
|