New modules shipped (from Linnet integration): - acoustic.py: AST (MIT/ast-finetuned-audioset-10-10-0.4593) replaces YAMNet stub; 527 AudioSet classes mapped to queue/speaker/environ/scene labels; _LABEL_MAP includes hold_music, ringback, DTMF, background_shift, AMD signal chain - accent.py: facebook/mms-lid-126 language ID → regional accent labels (en_gb, en_us, en_au, fr, es, de, zh, …); lazy-loaded, gated by CF_VOICE_ACCENT - privacy.py: compound privacy risk scorer — public_env, background_voices, nature scene, accent signals; returns 0–3 score without storing any audio - prosody.py: openSMILE-backed prosody extractor (sarcasm_risk, flat_f0_score, speech_rate, pitch_range); mock mode returns neutral values - dimensional.py: audeering/wav2vec2-large-robust-12-ft-emotion-msp-dim valence/arousal/dominance scorer; gated by CF_VOICE_DIMENSIONAL - trajectory.py: rolling buffer for arousal/valence deltas, trend detection (escalating/suppressed/stable), coherence scoring, suppression/reframe flags - telephony.py: TelephonyBackend Protocol + MockTelephonyBackend + SignalWireBackend + FreeSWITCHBackend; CallSession dataclass; make_telephony() factory - app.py: FastAPI service (port 8007) — /health + /classify; accepts base64 PCM chunks, returns full AudioEventOut including dimensional/prosody/accent fields - prefs.py: voice preference helpers (elcor_mode, confidence_threshold, whisper_model, elcor_prior_frames); cf-core and env-var fallback Tests: fix stale tests (YAMNetAcousticBackend → ASTAcousticBackend, scene field added to AcousticResult, speaker_at gap now resolves dominant speaker not UNKNOWN, make_io real path returns MicVoiceIO when sounddevice installed). 78 tests passing. Closes #2, #3.
109 lines
4.1 KiB
Python
109 lines
4.1 KiB
Python
import os
|
|
import pytest
|
|
from cf_voice.prefs import (
|
|
PREF_CONFIDENCE_THRESHOLD,
|
|
PREF_ELCOR_MODE,
|
|
PREF_ELCOR_PRIOR_FRAMES,
|
|
PREF_WHISPER_MODEL,
|
|
get_confidence_threshold,
|
|
get_elcor_prior_frames,
|
|
get_voice_pref,
|
|
get_whisper_model,
|
|
is_elcor_enabled,
|
|
set_voice_pref,
|
|
)
|
|
|
|
|
|
class _DictStore:
|
|
"""In-memory preference store for testing."""
|
|
|
|
def __init__(self, data: dict | None = None) -> None:
|
|
self._data: dict = data or {}
|
|
|
|
def get(self, user_id, path, default=None):
|
|
return self._data.get(path, default)
|
|
|
|
def set(self, user_id, path, value):
|
|
self._data[path] = value
|
|
|
|
|
|
class TestGetVoicePref:
|
|
def test_returns_default_when_nothing_set(self, monkeypatch):
|
|
monkeypatch.delenv("CF_VOICE_ELCOR", raising=False)
|
|
val = get_voice_pref(PREF_ELCOR_MODE, store=_DictStore())
|
|
assert val is False
|
|
|
|
def test_explicit_store_takes_priority(self):
|
|
store = _DictStore({PREF_ELCOR_MODE: True})
|
|
assert get_voice_pref(PREF_ELCOR_MODE, store=store) is True
|
|
|
|
def test_env_fallback_bool(self, monkeypatch):
|
|
monkeypatch.setenv("CF_VOICE_ELCOR", "1")
|
|
assert get_voice_pref(PREF_ELCOR_MODE, store=_DictStore()) is True
|
|
|
|
def test_env_fallback_false(self, monkeypatch):
|
|
monkeypatch.setenv("CF_VOICE_ELCOR", "0")
|
|
assert get_voice_pref(PREF_ELCOR_MODE, store=_DictStore()) is False
|
|
|
|
def test_env_fallback_float(self, monkeypatch):
|
|
monkeypatch.setenv("CF_VOICE_CONFIDENCE_THRESHOLD", "0.7")
|
|
val = get_voice_pref(PREF_CONFIDENCE_THRESHOLD, store=_DictStore())
|
|
assert abs(val - 0.7) < 1e-9
|
|
|
|
def test_env_fallback_int(self, monkeypatch):
|
|
monkeypatch.setenv("CF_VOICE_ELCOR_PRIOR_FRAMES", "6")
|
|
val = get_voice_pref(PREF_ELCOR_PRIOR_FRAMES, store=_DictStore())
|
|
assert val == 6
|
|
|
|
def test_env_fallback_str(self, monkeypatch):
|
|
monkeypatch.setenv("CF_VOICE_WHISPER_MODEL", "medium")
|
|
val = get_voice_pref(PREF_WHISPER_MODEL, store=_DictStore())
|
|
assert val == "medium"
|
|
|
|
def test_store_beats_env(self, monkeypatch):
|
|
monkeypatch.setenv("CF_VOICE_ELCOR", "1")
|
|
store = _DictStore({PREF_ELCOR_MODE: False})
|
|
# store has explicit False — but store.get returns None for falsy values
|
|
# only if the key is absent; here key IS set so store wins
|
|
store._data[PREF_ELCOR_MODE] = True
|
|
assert get_voice_pref(PREF_ELCOR_MODE, store=store) is True
|
|
|
|
def test_unknown_key_returns_none(self):
|
|
val = get_voice_pref("voice.nonexistent", store=_DictStore())
|
|
assert val is None
|
|
|
|
|
|
class TestSetVoicePref:
|
|
def test_sets_in_store(self):
|
|
store = _DictStore()
|
|
set_voice_pref(PREF_ELCOR_MODE, True, store=store)
|
|
assert store._data[PREF_ELCOR_MODE] is True
|
|
|
|
def test_no_store_raises(self, monkeypatch):
|
|
# Patch _cf_core_store to return None (simulates no cf-core installed)
|
|
import cf_voice.prefs as prefs_mod
|
|
monkeypatch.setattr(prefs_mod, "_cf_core_store", lambda: None)
|
|
with pytest.raises(RuntimeError, match="No writable preference store"):
|
|
set_voice_pref(PREF_ELCOR_MODE, True)
|
|
|
|
|
|
class TestConvenienceHelpers:
|
|
def test_is_elcor_enabled_false_default(self, monkeypatch):
|
|
monkeypatch.delenv("CF_VOICE_ELCOR", raising=False)
|
|
assert is_elcor_enabled(store=_DictStore()) is False
|
|
|
|
def test_is_elcor_enabled_true_from_store(self):
|
|
store = _DictStore({PREF_ELCOR_MODE: True})
|
|
assert is_elcor_enabled(store=store) is True
|
|
|
|
def test_get_confidence_threshold_default(self, monkeypatch):
|
|
monkeypatch.delenv("CF_VOICE_CONFIDENCE_THRESHOLD", raising=False)
|
|
assert get_confidence_threshold(store=_DictStore()) == pytest.approx(0.55)
|
|
|
|
def test_get_whisper_model_default(self, monkeypatch):
|
|
monkeypatch.delenv("CF_VOICE_WHISPER_MODEL", raising=False)
|
|
assert get_whisper_model(store=_DictStore()) == "small"
|
|
|
|
def test_get_elcor_prior_frames_default(self, monkeypatch):
|
|
monkeypatch.delenv("CF_VOICE_ELCOR_PRIOR_FRAMES", raising=False)
|
|
assert get_elcor_prior_frames(store=_DictStore()) == 4
|