Files
qtalker---/backend/tests/test_tts_piper.py
Indiana 58c30b7273 feat: possession presentation layer — glitchy text + degraded audio
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.
2026-07-23 05:48:02 +00:00

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