First sub-project of the "make contact feel real" arc (spec: docs/superpowers/specs/2026-07-23-possession-presentation-design.md). Direct Contact replies now feel like a spirit fighting through static to hold the channel rather than a plain chat bubble: - backend/app/possession.py: compute_stability(rarity, magnitude, rng) — a 0.05-0.98 score per reply (rarer entity + stronger triggering anomaly = cleaner signal), rng injectable for a later quantum-RNG source. - ws.py sends stability on reply_start; audio synthesis for that reply gets noise/bitcrush scaled by instability (1 - stability) via a new instability param on synthesize_spirit_voice — effects.py itself is untouched, only the params fed into it. - frontend/src/lib/possession.ts: renderPossessedText — pure, deterministic (tick-seeded, no Math.random) text corruption with self-correcting glitch bursts, wired into Transcript.tsx's streaming reply display. Stored transcript/reply text is unaffected — this is presentation only. 78/78 backend, 137/137 frontend tests passing.
119 lines
3.4 KiB
Python
119 lines
3.4 KiB
Python
import pytest
|
|
|
|
import app.tts.piper as piper
|
|
from app.tts.voices import VOICES
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def _fake_piper_synth(monkeypatch):
|
|
async def _fake_synthesize(self, text):
|
|
return b"fake-wav-bytes"
|
|
|
|
monkeypatch.setattr(piper.PiperTTS, "synthesize", _fake_synthesize)
|
|
|
|
|
|
def _capture_effects_call(monkeypatch):
|
|
calls = []
|
|
|
|
def _fake_apply_effects(wav_bytes, **kwargs):
|
|
calls.append(kwargs)
|
|
return wav_bytes
|
|
|
|
monkeypatch.setattr(piper, "apply_effects", _fake_apply_effects)
|
|
return calls
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_zero_instability_keeps_profile_defaults(monkeypatch):
|
|
calls = _capture_effects_call(monkeypatch)
|
|
voice = VOICES["lessac"]
|
|
|
|
await piper.synthesize_spirit_voice("hello", voice, {"noise": 0.03, "bitcrush": 0})
|
|
|
|
assert calls[0]["noise_level"] == pytest.approx(0.03)
|
|
assert calls[0]["bitcrush_bits"] == 0
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_instability_increases_noise_level(monkeypatch):
|
|
calls = _capture_effects_call(monkeypatch)
|
|
voice = VOICES["lessac"]
|
|
|
|
await piper.synthesize_spirit_voice(
|
|
"hello", voice, {"noise": 0.03, "bitcrush": 0}, instability=1.0
|
|
)
|
|
|
|
# Meaningfully louder than the clean baseline, but not blown out.
|
|
assert calls[0]["noise_level"] > 0.03
|
|
assert calls[0]["noise_level"] <= 0.1
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_noise_level_scales_monotonically_with_instability(monkeypatch):
|
|
voice = VOICES["lessac"]
|
|
noise_levels = []
|
|
for instability in (0.0, 0.25, 0.5, 0.75, 1.0):
|
|
calls = _capture_effects_call(monkeypatch)
|
|
await piper.synthesize_spirit_voice(
|
|
"hello", voice, {"noise": 0.03, "bitcrush": 0}, instability=instability
|
|
)
|
|
noise_levels.append(calls[0]["noise_level"])
|
|
|
|
assert noise_levels == sorted(noise_levels)
|
|
assert noise_levels[0] < noise_levels[-1]
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_bitcrush_stays_off_below_threshold(monkeypatch):
|
|
voice = VOICES["lessac"]
|
|
for instability in (0.0, 0.2, 0.5):
|
|
calls = _capture_effects_call(monkeypatch)
|
|
await piper.synthesize_spirit_voice(
|
|
"hello", voice, {"noise": 0.03, "bitcrush": 0}, instability=instability
|
|
)
|
|
assert calls[0]["bitcrush_bits"] == 0
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_bitcrush_kicks_in_above_threshold(monkeypatch):
|
|
calls = _capture_effects_call(monkeypatch)
|
|
voice = VOICES["lessac"]
|
|
|
|
await piper.synthesize_spirit_voice(
|
|
"hello", voice, {"noise": 0.03, "bitcrush": 0}, instability=1.0
|
|
)
|
|
|
|
assert calls[0]["bitcrush_bits"] > 0
|
|
assert calls[0]["bitcrush_bits"] < 16
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_instability_is_clamped_to_unit_range(monkeypatch):
|
|
calls = _capture_effects_call(monkeypatch)
|
|
voice = VOICES["lessac"]
|
|
|
|
await piper.synthesize_spirit_voice(
|
|
"hello", voice, {"noise": 0.03, "bitcrush": 0}, instability=5.0
|
|
)
|
|
clamped_high = calls[0]
|
|
|
|
calls = _capture_effects_call(monkeypatch)
|
|
await piper.synthesize_spirit_voice(
|
|
"hello", voice, {"noise": 0.03, "bitcrush": 0}, instability=1.0
|
|
)
|
|
at_one = calls[0]
|
|
|
|
assert clamped_high == at_one
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_instability_adds_on_top_of_profile_bitcrush(monkeypatch):
|
|
calls = _capture_effects_call(monkeypatch)
|
|
voice = VOICES["lessac"]
|
|
|
|
await piper.synthesize_spirit_voice(
|
|
"hello", voice, {"noise": 0.03, "bitcrush": 4}, instability=1.0
|
|
)
|
|
|
|
assert calls[0]["bitcrush_bits"] > 4
|