A complete System One model for emotion — emotion reading as typed decisions.
简体中文 | English
A standalone, general-purpose System One (Jev-class) model: one forward pass reads a message sequence (the v1/affect paradigm: each message may carry text form and/or PAD/pressure/vitality state snapshots) and emits calibrated probability distributions — PAD scores, an emotion family, yes/no judgments, and a confidence flag. It was born as the user-emotion front-end of the Chordia companion stack, and has since grown into an independent model that can serve any application.
In a string instrument, vibrato is how emotion travels through the note.
Applications read user emotion in many contexts — companion AI, chat analytics, moderation, mental-health triage, adaptive interfaces. The default today is an LLM call per message: slow, costly, privacy-leaking, and it returns three bare numbers. Vibrato is the System-One answer (the model class pioneered by TypeSafe Jev, with open kin like Laya and Tev1):
| LLM-per-message | Vibrato | |
|---|---|---|
| Latency | 3.7–13.2 s (measured across twelve challengers) | head 2.97 ms CPU-int8 / 1.92 ms GPU / full path ~16 ms GPU, ~144 ms CPU (all measured) |
| Output | 3 point numbers | full distributions + confidence |
| Cost / privacy | 8k–36k tokens per 40 cases, text leaves the box | $0, never leaves the box |
| Calibration | none | ordinal bins + entropy + conf head |
The teacher LLM only exists offline — the runtime is a 1.86M judgment head + a frozen embedding backbone (118M e5-small suffices; 568M bge-m3 is overkill).
Three question types, one forward pass:
| Type | Output | Content |
|---|---|---|
| score | 3 × 9-bin | Pleasure / Arousal / Dominance, each an ordinal distribution (Likert 1–9) |
| choice | OCC-22 ranked | 22 emotion types sorted by PAD→centroid distance (Ortony-Clore-Collins taxonomy); 8-family head used internally as training signal |
| noul | 2 flags | suppressed (irony / repression) · directed_at_me (target attribution) — the only two PAD cannot derive |
Each noul is a host-side behavioral signal (not a routing rule). A conf head flags low-confidence samples for escalation to a real LLM — System One means knowing when to abstain.
Score bins use the same 1–9 Likert scale as the classic 18-item PAD scale, so expected values drop into the standard (mean − 5) / 4 normalization unchanged. Pressure is derived, not predicted: ΔPressure = 1.0·(−ΔP) + 0.8·(ΔA) + 0.6·(−ΔD) (host-side accumulation; anti-runaway rule: accumulate the raw, unmodulated ΔPAD).
Jev pioneered the System One interface paradigm; this project defines the "mouth shape" of the affect front-end — messages in, distributions out:
POST /v1/affect/score
messages: [{ role: "user"|"assistant",
message?: string // text form (verbatim)
pad?: [p,a,d] // state snapshot [-1,1] (that speaker, that turn)
pressure?: number // pressure snapshot [0,100] (chordia standard)
vitality?: number // vitality snapshot [-30,100] (chordia-v2) }]
→ score×3 (9-bin PAD) + choice (OCC-22 ranked) + noul×2 (suppressed, directed_at_me)
+ conf + write_back{pad, d_pressure_raw} + consumed{fields, digest}
- Last message = the target (must be
user+message, no state fields — those are the answer, not the input) - All-text arrays = the generic-agent path, fully in-distribution — state is a precision knob, not an entry ticket
- Two-layer versioning: wire-format digest (
vib_messages.schema_digest(), v1 frozen, additive-only) is separate from model digestion (CONSUMED_FIELDS; v6 reads message/pad/pressure, vitality is carried-but-masked) - Units discipline: the wire speaks domain-native units; normalization is the model's private business
- Echo dissolved: the caller writes the previous output back onto the previous user message's
padfield (plus a fast cls-anchored pathway inside the net) - Scene break = state reset: across scenes/sessions (time passing, environment change) the host simply omits the pad/pressure fields — the reader cold-starts with no history. Inherit when continuous, reset when broken; state inheritance is the host's call (accumulation within one continuous conversation is the point)
v1/affect messages ─► text units (char ids) + state pseudo-tokens (5 vals + 5 mask)
+ frozen bge-m3 per-char features
│
echo: prev user PAD ───┼─► cls anchor ─► 3-layer transformer (d192, hand-written MHA, 512 window)
│ ├─ 3× bin heads (softmax over 1–9)
│ ├─ 8-way family head
│ ├─ 5× noul heads (2-way softmax)
└───────────────────┴─ conf head (sigmoid)
- Dual state pathways: echo anchored directly on cls (a zero-discovery-cost prior) plus in-sequence state pseudo-tokens (fine-grained, time-ordered context) — measured: the same information is worth +0.28 family accuracy via cls vs +0.06 in-sequence; both coexist
- Knowledge injection (pluggable backbone): a frozen embedding model provides per-char features into
in_proj. Backbone comparison (same pipeline, independent referee):
| Backbone | Params | dim | r̄ | P | A | D |
|---|---|---|---|---|---|---|
| bge-m3 | 568M | 1024 | 0.635 | 0.812 | 0.506 | 0.588 |
| e5-small (MIT) | 118M | 384 | 0.652 | 0.758 | 0.616 | 0.581 |
| bekko-a25m (MIT) | 123M | 384 | 0.582 | 0.769 | 0.438 | 0.539 |
| bekko-a8m (MIT) | 106M | 384 | 0.540 | 0.679 | 0.462 | 0.479 |
e5-small (118M) beats bge-m3 (568M) on r̄ and dominates A — the full bundle shrinks 417→85 MB. Swapping = 44s retrain, architecture untouched. 15% bge-dropout keeps the text-only path alive.
- 1.86M-parameter head, ONNX-friendly (hand-written MHA avoids export pitfalls); v5 head measured: CPU-ONNX-int8 single-thread 2.25 ms, 1.7 MB after int8 (v6 export/bench pending)
- Every artifact is guarded by contract digests (scale / battery / affect — three fingerprints); train/deploy sides must agree or refuse to run
Forty blind cases, fourteen contestants (each LLM answers per-case with the same battery prompt; sheets cost 26.7k–79.4k tokens each), plus an independent referee panel. Three yardsticks, each with a different bias — read them together.
Pearson r per PAD dimension against a dual-referee panel: two context-free blind raters (identical prompts, isolated sessions; self-agreement r = 0.98/0.98/0.97, family 0.82, noul 0.94), dual-mean PAD. Caveats: raters are GLM-family (dialect affinity with glm-5.3), n = 40.
| Contestant | r̄ (P/A/D) | latency/case | tokens |
|---|---|---|---|
| GLM-5.3 | 0.902 (0.94/0.84/0.93) | 4.7 s | 31.2k |
| gemini-3.8-flash | 0.866 (0.95/0.76/0.88) | 8.8 s | 60.3k |
| claude-sonnet-5.5 | 0.857 (0.92/0.74/0.92) | 4.7 s | 51.1k |
| grok-4.1-fast | 0.841 (0.94/0.74/0.85) | 3.4 s | 26.7k |
| Qwen3.8-27B | 0.807 | 12.8 s | 35.6k |
| gpt-5-mini | 0.762 | 13.2 s | 79.4k |
| deepseek-v4-flash | 0.716 | 3.7 s | 32.7k |
| Vibrato v0.1.1 (1.86M, local) | 0.635 (0.81/0.51/0.59) | 0.17 s | 0 |
| Clef-27B (System One, Apache-2.0) | 0.621 (0.84/0.66/0.36) | ~1.7 s | 0 (self-host) |
| Hunyuan-A13B / ref4b-4B / gpt-4o-mini | 0.59–0.62 | — | — |
| Clef-flash-9B (System One, Apache-2.0) | 0.564 (0.84/0.54/0.31) | ~1.3 s | 0 (self-host) |
| GLM-4-32B / Qwen2.5-7B / Xing4.0-29B | 0.43–0.47 | — | — |
The A dimension is a generational fault line: current-gen models reach 0.68–0.84 while the previous generation collapses (0.01–0.28). Vibrato's 0.506 tops the old camp but hasn't crossed into the new one.
Word-decode/noul agreement with Vibrato v0.1.1. Home-field note: the 40 cases are Vibrato's own battery distribution; LLMs answer zero-shot. Agreement ≠ accuracy — Qwen2.5-7B shows 0.90 top2 agreement with Vibrato while scoring 0.438 on Board A (style alliance, not skill).
| Contestant | top1 / top2 / noul vs vib |
|---|---|
| Qwen2.5-7B (daily driver) | 0.72 / 0.90 / 0.82 |
| claude-sonnet-5.5 | 0.70 / 0.80 / 0.75 |
| Xing4.0-29B | 0.70 / 0.85 / 0.76 |
| deepseek-v4-flash | 0.68 / 0.78 / 0.79 |
| Clef-27B | 0.72 / 0.97 / — |
| Clef-flash-9B | 0.75 / 1.00 / — |
| grok-4.1-fast | 0.62 / 0.70 / 0.76 |
| gemini-3.8-flash | 0.60 / 0.72 / 0.82 |
| Qwen3.8-27B / glm-5.3 / gpt-5-mini / gpt-4o-mini | 0.53–0.57 |
| Hunyuan-A13B / GLM-4-32B | 0.35–0.47 |
Full sheets (answers_*.jsonl) and one-command re-judging (judge.py, vs_ref.py --ref <tag>) ship in the repo.
Escalating the lowest-conf X% of cases to an LLM, rest Vibrato (r̄ vs the referee panel):
| escalate | API calls /40 | → sonnet-5.5 | → glm-5.3 |
|---|---|---|---|
| 0% | 0 | 0.635 | 0.635 |
| 20% | 8 | 0.652 | 0.667 |
| 50% | 20 | 0.713 | 0.753 |
Honest read: the mechanism works mechanically (+0.12 at 50%), but the 10% point is flat — the conf head (supervised by teacher self-consistency) currently flags teacher disagreement, not Vibrato's errors. Calibration against referee disagreement is queued.
Standard benchmarks (Chinese-EmoBank CVAS, 2,583 sentences, Pearson r):
| Config | Valence | Arousal |
|---|---|---|
| Zero-shot (cross-domain: chat-tuned → literary text) | 0.517 | 0.098 |
| 5-fold CV fine-tune (2k sentences × 4 epochs per fold) | 0.709±0.017 | 0.383±0.027 |
| Academic in-domain waterline (SemEval-2026 / EmoBank family) | ~0.70 | ~0.45 |
CPED (Chinese ERC standard set, 13→8 taxonomy zero-shot mapping): acc 0.222 / macro-F1 0.177 (majority baseline 0.165; anger recall 0.66 transfers best).
In-house battery val (8 families + PAD + noul, clean split): famAcc 0.61–0.67 / noulAcc 0.79 / padMAE 0.21–0.23.
The benchmark's most valuable finding isn't any single score — it's that System One models (Vibrato 1.86M, Clef 27B, Clef-flash 9B) form a behaviorally coherent cluster (mutual top-2 agreement 0.97–1.00) that reads emotion differently from LLMs. Three observations explain why:
1. Gestalt pattern-matching, not autoregressive reasoning. LLMs "read" text — they parse semantics, infer causality, then classify ("he used X word in Y context, therefore probably angry"). System One models skip the reasoning step entirely. Whether it's a 1.86M char-level transformer (Vibrato) or a frozen Qwen with its generative pathway locked (Clef), both map input directly to typed decisions via pattern-matching — closer to an amygdala than a cortex. This is why System One latency is measured in milliseconds, not seconds.
2. Immunity to the alignment tax. Why do LLMs collectively collapse on the D dimension (dominance: 0.28–0.36 for most, vs 0.588 for Vibrato)? RLHF trains LLMs to be polite, neutral, and objective — which blunts their sensitivity to power dynamics, passive aggression, and dominance signals in text. System One models carry no such baggage. Vibrato's 61k legacy training data preserves the raw distribution of how language exerts pressure, without social filtering. This is a feature, not a bug: a companion AI that can't sense "I'm being talked down to" from text is emotionally deaf.
3. Dimensional convergence proves emotional intuition is low-dimensional. The most striking result: clef-flash (9B frozen Qwen) and Vibrato (1.86M char-level) achieve top2 = 1.00 — clef's top-1 word always appears in Vibrato's top-2. When you take a 9B model and strip away its autoregressive "rational brain," forcing it to classify through routing heads only, its decision surface collapses onto nearly the same manifold as a 1.86M character-level model. This is mathematical evidence that emotional intuition is a compact, learnable feature space — it doesn't require billions of parameters to represent, only the right training signal.
demo/vibrato_demo.html — a chat-style bubble stream where every message carries a Vibrato sidecar card (measured PAD tri-bars, family badge, noul flags; the small hatched bars are the script's hand-crafted gold), with a four-line trajectory chart on the right:
- Linked arm: Vibrato measures the user turn-by-turn → the Chordia v2 engine (7-dim MLP: user PAD + vitality + agent PAD → ΔPAD) drives the agent's dynamics
- LLM self-enactment arm: same personality, one system prompt — the LLM imagines the whole run and self-reports its PAD
Two personalities × three acts × 20 turns (anger confrontation → breakdown & recovery → rivalry to friendship). What the measured run shows: the linked loop has accumulation and hysteresis (the timid persona gets dragged into a deep empathic collapse [-1.0, -0.69] and recovers late; the cheerful persona at vitality 90 dips to -0.6 and rebounds to +0.31), while self-enactment oscillates mildly per message (the cheerful persona barely dips to -0.3 while the user sobs; both LLM personalities run near-parallel tracks — personality doesn't modulate the dynamics). Measured user PAD vs hand-crafted gold: r(P/A/D) = 0.74 / 0.52 / 0.35. A known flaw is visible too: recovery after a long hostile run lags (state-anchor drag).
Regenerate with python demo/demo_html.py [--llm --key sk-..] (linked arm is zero-API; the --llm arm needs an OpenAI-compatible endpoint).
| File | Role |
|---|---|
pad_schema.py |
Frozen scale contract: bins, 18-item bridge (tent kernel, expectation-preserving), pressure formula, digests |
battery.py |
Full question battery + teacher prompt builder + gold validation |
vib_messages.py |
v1/affect wire format: validation, state vectors, form-dropout augmentation, dual rendering, dual digests |
net.py |
VibratoNet v6 (state pseudo-tokens + echo anchor + feats pathway) + collate_pad + self-test |
vib_state.py |
v5 context rendering (legacy bridge) |
feat_bge.py / feat_any.py |
Frozen bge-m3 / any-HF-model per-char features |
eval_backbone.py |
Backbone comparison: same pipeline, different embeddings, same referee |
gen_convos.py / label_pad.py / label_agent.py |
Conversation synthesis / user-side teacher labeling / agent-side labeling |
prep.py / feat_precompute.py / train.py |
data assembly (6-message parity window) / bge feature memmap / two-stage training |
judge.py / answer_sheet.py / eval_bench.py / finetune_cvas.py |
Offline judge (answer-sheet architecture) / sheet collection / standard benchmarks / CVAS 5-fold |
demo/ |
The linkage demo (HTML generator, personality script, Chordia engine + checkpoint) |
| Package | Size | Contents |
|---|---|---|
vibrato-v0.1.1-dist.zip |
17 MB | Code + weights + ONNX (bge downloadable) |
vibrato-bundle-v0.1.1.zip |
417 MB | Full-knowledge, bge-m3 int8 (separate files) |
vibrato-fused-v0.1.1.zip |
410 MB | Fused single-file ONNX (one session.run) |
vibrato-e5s-v0.1.1.zip |
85 MB | Lightweight full-knowledge (e5-small, A-dim strongest) |
All bundles: zero torch dependency (onnxruntime + tokenizers + numpy).
pip install torch # smoke tests need nothing else
python pad_schema.py # scale contract self-test (v6 digest)
python vib_messages.py # v1/affect paradigm self-test
python net.py # forward + backward + state/echo-activity smoke test
python battery.py # battery self-test + digestsFull v6 pipeline (data stages need any OpenAI-compatible endpoint; training needs a GPU): gen_convos → label_pad → label_agent → prep_v6 → feat_precompute → train.py (mixed stage 1) → train.py --battery-only --init (stage 2) → judge.py. Every data stage writes incrementally and resumes — crash-safe by design.
v0.1.1. The full chain (paradigm → data → two-stage training → judging → standard benchmarks) runs end-to-end with everything measured. Known limitations:
- Perceptual inertia: right direction, miscalibrated magnitude — after 7+ hostile turns, a suddenly calm user does not flip the reading (human listeners do not believe instant recoveries either — suspicion of residual anger or passive aggression is the realistic read). That is the correct direction. But the current decay is too slow: vib trails a recovery far longer than a human reader would (humans update within 2–3 turns). The primary lever is scene break = state reset (see the v1/affect section) — the host omits the pad/pressure fields across scenes/sessions and the reader cold-starts. Anchor-decay tuning is off the roadmap: the inertia direction is a feature, the model stays untouched, inheritance belongs to the host.
- Arousal (A) is backbone-dependent — with bge-m3 Vibrato scores 0.506; swapping to e5-small lifts A to 0.616 (best arousal across all experiments, entering the range of current-gen LLMs 0.68–0.84). Remaining gap likely training-data distribution, not backbone capacity. Conf head calibration queued.
v6 ONNX export pendingdone: full-input-signature ONNX (model_v6/, zero-feats = the text-only path trained in via bge-dropout), int8 = 2.2 MB, PAD deviation <0.005. A CPU-only full-knowledge tier (bge int8) is still unmeasured.- Cross-taxonomy zero-shot (CPED/GoEmotions style) is still weak (0.22 acc) — what v0.1.1 validated is that light adaptation reaches the table (the CVAS line).
- The System One model class: TypeSafe Jev for the idea; Laya (Apache-2.0) for score-type markers and RLCD training lore; Tev1 (MIT) for the cheap-SFT recipe.
- Architecture lineage (hand-written MHA, echo rounds, arbiter discipline) comes from the author's own Prisma segmentation stack.
- The Chordia v2 emotion dynamics engine and personality test scenarios in
demo/come from the author's Chordia project. - Origin: Vibrato began life as the user-emotion front-end of the Chordia companion system, and grew into a standalone System One model.
MIT © 2026 corolin