cf-voice/tests/test_models.py
pyr0ball 35fc0a088c feat: initial cf-voice stub — VoiceFrame API, mock IO, context classifier
- 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
2026-04-06 16:03:07 -07:00

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