- VoiceFrame dataclass: label, confidence, speaker_id, shift_magnitude, timestamp - MockVoiceIO: async generator of synthetic frames on a timer (CF_VOICE_MOCK=1) - ContextClassifier: passthrough stub wrapping VoiceIO; _enrich() hook for real classifiers - make_io() factory: mock mode auto-detected from env, raises NotImplementedError for real audio - cf-voice-demo CLI entry point for quick smoke-testing - 12 tests passing; editable install via pip install -e ../cf-voice
104 lines
3.3 KiB
Python
104 lines
3.3 KiB
Python
import asyncio
|
|
import pytest
|
|
from cf_voice.models import VoiceFrame
|
|
from cf_voice.io import MockVoiceIO, make_io
|
|
from cf_voice.context import ContextClassifier
|
|
|
|
|
|
def make_frame(**kwargs) -> VoiceFrame:
|
|
defaults = dict(
|
|
label="Calm and focused",
|
|
confidence=0.8,
|
|
speaker_id="speaker_a",
|
|
shift_magnitude=0.0,
|
|
timestamp=1.0,
|
|
)
|
|
return VoiceFrame(**{**defaults, **kwargs})
|
|
|
|
|
|
class TestVoiceFrame:
|
|
def test_is_reliable_above_threshold(self):
|
|
assert make_frame(confidence=0.7).is_reliable(threshold=0.6)
|
|
|
|
def test_is_reliable_below_threshold(self):
|
|
assert not make_frame(confidence=0.4).is_reliable(threshold=0.6)
|
|
|
|
def test_is_shift_above_threshold(self):
|
|
assert make_frame(shift_magnitude=0.5).is_shift(threshold=0.3)
|
|
|
|
def test_is_shift_below_threshold(self):
|
|
assert not make_frame(shift_magnitude=0.1).is_shift(threshold=0.3)
|
|
|
|
def test_default_reliable_threshold(self):
|
|
assert make_frame(confidence=0.6).is_reliable()
|
|
assert not make_frame(confidence=0.59).is_reliable()
|
|
|
|
|
|
class TestMockVoiceIO:
|
|
@pytest.mark.asyncio
|
|
async def test_emits_frames(self):
|
|
io = MockVoiceIO(interval_s=0.05, seed=42)
|
|
frames = []
|
|
async for frame in io.stream():
|
|
frames.append(frame)
|
|
if len(frames) >= 3:
|
|
await io.stop()
|
|
break
|
|
assert len(frames) == 3
|
|
assert all(isinstance(f, VoiceFrame) for f in frames)
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_confidence_in_range(self):
|
|
io = MockVoiceIO(interval_s=0.05, seed=1)
|
|
count = 0
|
|
async for frame in io.stream():
|
|
assert 0.0 <= frame.confidence <= 1.0
|
|
assert 0.0 <= frame.shift_magnitude <= 1.0
|
|
count += 1
|
|
if count >= 5:
|
|
await io.stop()
|
|
break
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_timestamps_increase(self):
|
|
io = MockVoiceIO(interval_s=0.05, seed=0)
|
|
timestamps = []
|
|
async for frame in io.stream():
|
|
timestamps.append(frame.timestamp)
|
|
if len(timestamps) >= 3:
|
|
await io.stop()
|
|
break
|
|
assert timestamps == sorted(timestamps)
|
|
|
|
def test_make_io_mock_env(self, monkeypatch):
|
|
monkeypatch.setenv("CF_VOICE_MOCK", "1")
|
|
io = make_io()
|
|
assert isinstance(io, MockVoiceIO)
|
|
|
|
def test_make_io_real_raises(self, monkeypatch):
|
|
monkeypatch.delenv("CF_VOICE_MOCK", raising=False)
|
|
with pytest.raises(NotImplementedError):
|
|
make_io(mock=False)
|
|
|
|
|
|
class TestContextClassifier:
|
|
@pytest.mark.asyncio
|
|
async def test_mock_passthrough(self):
|
|
classifier = ContextClassifier.mock(interval_s=0.05, seed=7)
|
|
frames = []
|
|
async for frame in classifier.stream():
|
|
frames.append(frame)
|
|
if len(frames) >= 3:
|
|
await classifier.stop()
|
|
break
|
|
assert len(frames) == 3
|
|
assert all(isinstance(f, VoiceFrame) for f in frames)
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_from_env_mock(self, monkeypatch):
|
|
monkeypatch.setenv("CF_VOICE_MOCK", "1")
|
|
classifier = ContextClassifier.from_env(interval_s=0.05)
|
|
async for frame in classifier.stream():
|
|
assert isinstance(frame, VoiceFrame)
|
|
await classifier.stop()
|
|
break
|