Contracts (the shared language): - percept / state / context / utterance / render / audio / capabilities - Capability model instead of vendor schemas (see below) Providers (capability-based abstraction): - LLM + Embedding: OpenAI-compatible shape IS the standard, adopted directly - ASR: internal contract is an event stream (partial -> final), because /v1/audio/transcriptions has no partial and dialogue needs it for endpointing. OpenAI adapter degrades to supports_partial=False. - TTS: internal contract carries tags/emotion/style, because /v1/audio/speech has no paralinguistics and [laughter]/[breath] are the core of feeling alive. Unsupported providers are stripped AND reported via Degradation -- silent degradation is forbidden. - Avatar: no de-facto standard, custom RenderCmd protocol driven by State. Core: - mood: dynamical system, slow variables, negativity bias (grudge), numbing after prolonged negativity - bazi: four pillars -> numeric vector; 'ming' is data, not prose - projection: phi, system event -> in-world statement; uncovered events are counted as projection gaps Storage: - SQLite schema, three memory layers, dyad ledger - accessibility floor 0.03 (not 0): forgettable but still there Config split: - models.yaml is the ONLY place model versions live - character.yaml holds world rules / emotion params, no version numbers 58 tests pass; ruff clean.
118 lines
3.1 KiB
Python
118 lines
3.1 KiB
Python
"""契约层测试。
|
||
|
||
这些测试守的是"接口形状",不是实现细节。
|
||
接口一变,它们就该红——因为所有层都依赖这些形状。
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
from cybergril.contracts import (
|
||
Capabilities,
|
||
Degradation,
|
||
Expression,
|
||
Modality,
|
||
ParaEmotion,
|
||
ParaEvent,
|
||
Paralinguistics,
|
||
Percept,
|
||
SpeechTag,
|
||
State,
|
||
Streaming,
|
||
Utterance,
|
||
)
|
||
|
||
|
||
def test_state_clamp_bounds() -> None:
|
||
s = State(v=5.0, a=3.0, d=-1.0, e=2.0)
|
||
s.clamp()
|
||
assert -1.0 <= s.v <= 1.0
|
||
assert 0.0 <= s.a <= 1.0
|
||
assert 0.0 <= s.d <= 1.0
|
||
assert 0.0 <= s.e <= 1.0
|
||
|
||
|
||
def test_state_energy_band() -> None:
|
||
assert State(e=0.1).energy_band() == "exhausted"
|
||
assert State(e=0.3).energy_band() == "tired"
|
||
assert State(e=0.5).energy_band() == "normal"
|
||
assert State(e=0.9).energy_band() == "energized"
|
||
|
||
|
||
def test_utterance_strip_tags_removes_inline() -> None:
|
||
u = Utterance(
|
||
text="真的吗[laughter]……我、我先想想[breath]",
|
||
speech_tags=(SpeechTag.LAUGHTER, SpeechTag.BREATH),
|
||
)
|
||
stripped = u.strip_tags()
|
||
assert "[laughter]" not in stripped.text
|
||
assert "[breath]" not in stripped.text
|
||
assert stripped.speech_tags == ()
|
||
# 原文不变(frozen)
|
||
assert "[laughter]" in u.text
|
||
|
||
|
||
def test_utterance_tag_density() -> None:
|
||
u = Utterance(text="好" * 100, speech_tags=(SpeechTag.LAUGHTER,))
|
||
assert abs(u.tag_density() - 0.01) < 1e-9
|
||
|
||
|
||
def test_capabilities_tag_support() -> None:
|
||
caps = Capabilities(
|
||
modality=Modality.AUDIO,
|
||
streaming=Streaming.CHUNKED,
|
||
speech_tags=frozenset({"laughter", "breath"}),
|
||
)
|
||
assert caps.tags_supported({"laughter"})
|
||
assert not caps.tags_supported({"laughter", "sigh"})
|
||
|
||
|
||
def test_capabilities_can() -> None:
|
||
caps = Capabilities(modality=Modality.TEXT, emotion_control=True)
|
||
assert caps.can("emotion_control")
|
||
assert not caps.can("voice_clone")
|
||
|
||
|
||
def test_degradation_readable() -> None:
|
||
d = Degradation(
|
||
capability="speech_tags",
|
||
requested=("sigh",),
|
||
reason="provider 不支持",
|
||
suggestion="换 provider",
|
||
)
|
||
text = str(d)
|
||
assert "degraded" in text
|
||
assert "speech_tags" in text
|
||
assert "换 provider" in text
|
||
|
||
|
||
def test_paralinguistics_distress() -> None:
|
||
calm = Paralinguistics(emotion=ParaEmotion.HAPPY)
|
||
assert not calm.is_distressed()
|
||
|
||
crying = Paralinguistics(
|
||
emotion=ParaEmotion.SAD, events=frozenset({ParaEvent.CRY})
|
||
)
|
||
assert crying.is_distressed()
|
||
assert crying.is_negative()
|
||
|
||
|
||
def test_percept_factories() -> None:
|
||
p = Percept.asr_final(ts=1000, text="你好", confidence=0.9)
|
||
assert p.kind.value == "asr_final"
|
||
assert p.text() == "你好"
|
||
assert p.confidence == 0.9
|
||
|
||
sys_p = Percept.system(ts=0, event="net.error")
|
||
assert sys_p.system_event() == "net.error"
|
||
|
||
|
||
def test_expression_from_state() -> None:
|
||
e = Expression.from_state(v=0.8, a=0.9, d=0.2, e=0.1)
|
||
assert e.happy > 0
|
||
assert e.tired > 0.8
|
||
assert e.surprise > 0
|
||
|
||
e2 = Expression.from_state(v=0.0, a=0.3, d=0.8, e=0.9)
|
||
assert e2.happy == 0.0
|
||
assert e2.sad == 0.0
|