SpellSpeak Tone rc4
Browse files- MANIFEST.json +22 -22
- README.md +61 -48
- config.json +25 -3
- example.py +7 -4
- model.onnx +2 -2
- model.opt.onnx +2 -2
- model_fp16.onnx +2 -2
- runtime/contracts/schemas/line_tags.py +40 -0
- runtime/harness/expression/calibrate.py +7 -0
- runtime/harness/expression/classifier.py +27 -6
MANIFEST.json
CHANGED
|
@@ -1,33 +1,33 @@
|
|
| 1 |
{
|
| 2 |
"name": "SpellSpeak Tone",
|
| 3 |
-
"release": "
|
| 4 |
-
"package": "spellspeak-tone-
|
| 5 |
-
"created": "2026-10-
|
| 6 |
-
"contract": "line-tags-0.
|
| 7 |
"files": {
|
| 8 |
"README.md": {
|
| 9 |
-
"bytes":
|
| 10 |
-
"sha256": "
|
| 11 |
},
|
| 12 |
"config.json": {
|
| 13 |
-
"bytes":
|
| 14 |
-
"sha256": "
|
| 15 |
},
|
| 16 |
"example.py": {
|
| 17 |
-
"bytes":
|
| 18 |
-
"sha256": "
|
| 19 |
},
|
| 20 |
"model.onnx": {
|
| 21 |
-
"bytes":
|
| 22 |
-
"sha256": "
|
| 23 |
},
|
| 24 |
"model.opt.onnx": {
|
| 25 |
-
"bytes":
|
| 26 |
-
"sha256": "
|
| 27 |
},
|
| 28 |
"model_fp16.onnx": {
|
| 29 |
-
"bytes":
|
| 30 |
-
"sha256": "
|
| 31 |
},
|
| 32 |
"requirements.txt": {
|
| 33 |
"bytes": 287,
|
|
@@ -42,8 +42,8 @@
|
|
| 42 |
"sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
|
| 43 |
},
|
| 44 |
"runtime/contracts/schemas/line_tags.py": {
|
| 45 |
-
"bytes":
|
| 46 |
-
"sha256": "
|
| 47 |
},
|
| 48 |
"runtime/harness/__init__.py": {
|
| 49 |
"bytes": 0,
|
|
@@ -54,12 +54,12 @@
|
|
| 54 |
"sha256": "faed14f74aaded53734860d18b128475e8724c6961b978fc6100ae533ee2c38c"
|
| 55 |
},
|
| 56 |
"runtime/harness/expression/calibrate.py": {
|
| 57 |
-
"bytes":
|
| 58 |
-
"sha256": "
|
| 59 |
},
|
| 60 |
"runtime/harness/expression/classifier.py": {
|
| 61 |
-
"bytes":
|
| 62 |
-
"sha256": "
|
| 63 |
},
|
| 64 |
"runtime/harness/expression/decide.py": {
|
| 65 |
"bytes": 1410,
|
|
|
|
| 1 |
{
|
| 2 |
"name": "SpellSpeak Tone",
|
| 3 |
+
"release": "rc4",
|
| 4 |
+
"package": "spellspeak-tone-rc4.1",
|
| 5 |
+
"created": "2026-10-13T21:14:11Z",
|
| 6 |
+
"contract": "line-tags-0.3",
|
| 7 |
"files": {
|
| 8 |
"README.md": {
|
| 9 |
+
"bytes": 14522,
|
| 10 |
+
"sha256": "9991b7a10156b5b26bcb11220db3347f73f502ffc11e0b356402fcd327a55963"
|
| 11 |
},
|
| 12 |
"config.json": {
|
| 13 |
+
"bytes": 1933,
|
| 14 |
+
"sha256": "61eea9b2b6c9463460319e7e484f675b8558f4e7ec87439bbf6cf2042a65c2a2"
|
| 15 |
},
|
| 16 |
"example.py": {
|
| 17 |
+
"bytes": 3225,
|
| 18 |
+
"sha256": "8ddf42e1dbd41c4f75e7f23f0ea21e7bcdd4a723f92e2fc4e15b646a9726d4e0"
|
| 19 |
},
|
| 20 |
"model.onnx": {
|
| 21 |
+
"bytes": 111766436,
|
| 22 |
+
"sha256": "0e4c32cae657161a27b2a1b486f1f78781351cbb6745d78bea4c1a7e7e7dbe3d"
|
| 23 |
},
|
| 24 |
"model.opt.onnx": {
|
| 25 |
+
"bytes": 111727815,
|
| 26 |
+
"sha256": "727035a576887a119e60dae1d6271b1f23b1f3fddf386cbd45b8cb38a9494730"
|
| 27 |
},
|
| 28 |
"model_fp16.onnx": {
|
| 29 |
+
"bytes": 55995133,
|
| 30 |
+
"sha256": "7d59948b90d24614da8b95a9e09c080d50e60feb32b7ecbbad3c22c537eb91a1"
|
| 31 |
},
|
| 32 |
"requirements.txt": {
|
| 33 |
"bytes": 287,
|
|
|
|
| 42 |
"sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
|
| 43 |
},
|
| 44 |
"runtime/contracts/schemas/line_tags.py": {
|
| 45 |
+
"bytes": 10306,
|
| 46 |
+
"sha256": "e48834e280559c1b181cf3fd544bad474f08cb257d42a9be8577fe9e968eb418"
|
| 47 |
},
|
| 48 |
"runtime/harness/__init__.py": {
|
| 49 |
"bytes": 0,
|
|
|
|
| 54 |
"sha256": "faed14f74aaded53734860d18b128475e8724c6961b978fc6100ae533ee2c38c"
|
| 55 |
},
|
| 56 |
"runtime/harness/expression/calibrate.py": {
|
| 57 |
+
"bytes": 2943,
|
| 58 |
+
"sha256": "09820027f15c353b20b2d1102d4858c6de7cf48b3ed10015e2d94da1becd5634"
|
| 59 |
},
|
| 60 |
"runtime/harness/expression/classifier.py": {
|
| 61 |
+
"bytes": 8759,
|
| 62 |
+
"sha256": "effed190273e2f4dcb7c8b73ea809058873a4d4464aa6e3eb9f85929f12b3bad"
|
| 63 |
},
|
| 64 |
"runtime/harness/expression/decide.py": {
|
| 65 |
"bytes": 1410,
|
README.md
CHANGED
|
@@ -13,6 +13,7 @@ tags:
|
|
| 13 |
- multi-party-conversation
|
| 14 |
- emotion-recognition
|
| 15 |
- dialogue-acts
|
|
|
|
| 16 |
- turn-taking
|
| 17 |
- real-time
|
| 18 |
- pipecat
|
|
@@ -20,28 +21,27 @@ tags:
|
|
| 20 |
- dialogue
|
| 21 |
---
|
| 22 |
|
| 23 |
-
# SpellSpeak Tone ·
|
| 24 |
|
| 25 |
When several AI characters share one conversation, as in a game, an interactive story or any other multi-character voice experience, each of them needs a structured reading of every line: how the speaker sounds, what the line does to each person present, and whether it leaves a question open for someone else to answer. The language model voicing a character could produce that reading, but only through extra output tokens, training or tool calls. That adds latency to every turn, takes capacity from the dialogue, risks malformed output, and repeats the work for each character in the scene.
|
| 26 |
|
| 27 |
-
SpellSpeak Tone treats the reading as multi-task classification by a separate small model. One pass over a line predicts the speaker's emotion, the social act directed at each person present with a hostility probability per person,
|
| 28 |
|
| 29 |
[Examples and source](https://github.com/spellspeak/models/tree/main/tone)
|
| 30 |
|
| 31 |
-
## What is new in
|
| 32 |
|
| 33 |
-
- **
|
| 34 |
-
- **
|
| 35 |
-
- **
|
| 36 |
-
- **What it costs.** Emotion macro-F1 on the main test is 0.649 against rc2's 0.677, most of it in rare emotions (disgust, fear). The move on lines between characters is weaker (0.633 against 0.754). Without who a line is said to, harmless player lines are called hostile more often (3.9% against 1.9%).
|
| 37 |
|
| 38 |
| | |
|
| 39 |
|---|---|
|
| 40 |
-
| **Released** | 2026-10-13 ·
|
| 41 |
-
| **What it does** | Reads one line of dialogue and tags the speaker's emotion, the social act toward each person present,
|
| 42 |
-
| **What changed from
|
| 43 |
-
| **Size** | 27.9M parameters · 111.
|
| 44 |
-
| **Speed** | p95 per line on 4 CPU threads (Apple M1 Max): 9.
|
| 45 |
| **Runtime** | ONNX Runtime (1.24 or later) and tokenizers, on CPU. A Python reference runtime ships with it |
|
| 46 |
| **Files** | `model.opt.onnx` (the graph to run), `model.onnx` (the same graph unoptimized), `model_fp16.onnx` (the graph to run with its weights in 16 bits), `tokenizer.json` and `config.json`, with the reference runtime (`runtime/`), `example.py` and `requirements.txt`. `MANIFEST.json` lists every file with its size and sha256 |
|
| 47 |
| **Licence** | Apache-2.0 |
|
|
@@ -50,13 +50,13 @@ SpellSpeak Tone treats the reading as multi-task classification by a separate sm
|
|
| 50 |
|
| 51 |
```bash
|
| 52 |
pip install -U huggingface_hub
|
| 53 |
-
hf download spellspeak/tone --revision
|
| 54 |
cd spellspeak-tone
|
| 55 |
pip install -r requirements.txt
|
| 56 |
python example.py
|
| 57 |
```
|
| 58 |
|
| 59 |
-
The example puts three characters and the player in a room. The player asks everyone a question, the characters answer in different ways, and
|
| 60 |
|
| 61 |
```
|
| 62 |
player to everyone: 'Who knows where the mill key is?'
|
|
@@ -73,12 +73,14 @@ player to Mara: "thanks, that's all i needed"
|
|
| 73 |
move closes (0.98) · emotion happy · acts: Mara thanks
|
| 74 |
player to Tomas: "Touch my horse again and you'll regret it."
|
| 75 |
move other (0.71) · emotion angry · acts: Tomas threat
|
|
|
|
|
|
|
| 76 |
```
|
| 77 |
|
| 78 |
The small download leaves out the two full-precision graphs and runs the 16-bit file. It prints the same tags:
|
| 79 |
|
| 80 |
```bash
|
| 81 |
-
hf download spellspeak/tone --revision
|
| 82 |
cd spellspeak-tone-small
|
| 83 |
pip install -r requirements.txt
|
| 84 |
python example.py --fp16
|
|
@@ -97,45 +99,52 @@ tone = LineClassifier(".") # full precision (model.opt.onnx), the default
|
|
| 97 |
# tone = LineClassifier(".", precision="fp16") # the 16-bit file (model_fp16.onnx), for the small download
|
| 98 |
tags = tone.tag(LineInput(speaker="player", speaker_kind="player", to="Mara", targets=["Mara", "Tomas"],
|
| 99 |
text="oh great, thanks a lot", previous="Mara: Sorry, we're out of rooms tonight."))
|
| 100 |
-
print(tags.emotion, tags.acts["Mara"]) #
|
| 101 |
```
|
| 102 |
|
| 103 |
## Input and output
|
| 104 |
|
| 105 |
- **Input:** the line, the line before it, who speaks (the player or a character), who it is said to (or the room), and everyone present (tested up to 8 people; 160 tokens at most). Who it is said to and the line before are optional, and help: see the table below. Sarcasm is mostly read from the line before.
|
| 106 |
-
- **Per line:** an emotion (neutral, happy, amused, angry, sad, afraid, surprised, disgusted, contemptuous) with intensity (low, medium, high) and a confidence. Whether the line is only a listening sound such as "mm-hm" (backchannel). The **move**, with a confidence.
|
| 107 |
- **Per person:** an act (insult, threat, accusation, tease, demand, dismiss, refusal, request, thanks, praise, apology, comfort, warning, none) with intensity and a confidence.
|
| 108 |
- **The moves.** asks: wants a reply or an action. answers: gives what was asked for, or offers information. hedges: offers something second-hand or unsure. doesnt_know: says it does not know. withholds: declines, deflects or dodges. closes: winds the exchange down and wants no reply. other: anything else.
|
| 109 |
- **Hostile** means insult, threat, accusation, or demand at high intensity. A tag is hostile when the hostility score reaches 0.675.
|
|
|
|
| 110 |
- **Confidence** is the chance the tag is right (calibrated). For a hostile tag it is the chance the line is hostile toward that person.
|
| 111 |
|
| 112 |
## Performance (held-out test sets, committee labels)
|
| 113 |
|
| 114 |
-
|
| 115 |
|
| 116 |
-
|
| 117 |
-
|---|---|---|
|
| 118 |
-
| Sarcastic lines hostile toward someone: caught (141 pairs) | 52% | 73% (21 points more; 95% interval 13 to 30) |
|
| 119 |
-
| Sarcastic lines read at face value (294) | 14% | 6% |
|
| 120 |
-
| Look-alikes and harmless sarcasm called hostile | 5.5% | 3.3% |
|
| 121 |
|
| 122 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 123 |
|
| 124 |
-
The emotion, the acts and the move
|
| 125 |
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
|
| 129 |
-
|
|
| 130 |
-
|
|
| 131 |
-
|
|
| 132 |
-
|
|
| 133 |
-
|
|
| 134 |
-
|
|
| 135 |
-
|
|
| 136 |
-
|
|
| 137 |
-
| Move macro-F1
|
| 138 |
-
|
|
|
|
|
|
|
|
| 139 |
|
| 140 |
For scale, two frontier labellers agree with each other at 0.766 act F1, 0.709 emotion F1 and 0.845 move F1.
|
| 141 |
|
|
@@ -145,6 +154,8 @@ The same test lines, scored again without who a line is said to, without the lin
|
|
| 145 |
|
| 146 |
| Measure | As given | Without `to` | Without the line before | Without either |
|
| 147 |
|---|---|---|---|---|
|
|
|
|
|
|
|
| 148 |
| Act macro-F1, main test | 0.661 | 0.564 | 0.656 | 0.552 |
|
| 149 |
| Emotion macro-F1, main test | 0.649 | 0.649 | 0.640 | 0.636 |
|
| 150 |
| Harmless lines called hostile, prose | 1.4% | 1.5% | 1.3% | 1.8% |
|
|
@@ -153,35 +164,37 @@ The same test lines, scored again without who a line is said to, without the lin
|
|
| 153 |
| Move macro-F1 | 0.816 | 0.805 | 0.722 | 0.683 |
|
| 154 |
| Answered or still open | 83.0% | 80.0% | 66.1% | 60.1% |
|
| 155 |
|
|
|
|
| 156 |
- **Send who a line is said to when you know it.** Without it, more harmless player lines are called hostile.
|
| 157 |
-
- **Send the line before.** Sarcasm and the move are both read from it.
|
| 158 |
|
| 159 |
### The 16-bit file
|
| 160 |
|
| 161 |
-
The same 5,204 test lines, tagged by `model_fp16.onnx` and by full precision, in the same four columns:
|
| 162 |
|
| 163 |
| | As given | Without `to` | Without the line before | Without either |
|
| 164 |
|---|---|---|---|---|
|
| 165 |
| Lines with every tag the same as full precision | 100% | 99.98% (one act toward one person differs) | 100% | 99.96% (two lines differ) |
|
|
|
|
| 166 |
|
| 167 |
-
Every measure in the tables above is the same with the 16-bit file. It computes in 32 bits, so it saves the download and the disk, not time or memory (
|
| 168 |
|
| 169 |
## How it was made
|
| 170 |
|
| 171 |
-
- **Model:** MiniLM-L6 (`sentence-transformers/all-MiniLM-L6-v2` at 1110a24, Apache-2.0) with six small heads. For the move, trainable copies of its top two layers read its fourth layer's output, and a small head reads them.
|
| 172 |
- **Distillation:** the emotion and the acts from three Ettin-1B teachers (`jhu-clsp/ettin-encoder-1b`, MIT) over 374,534 lines, plus 20,286 lines labelled directly. The move from rc2's three move teachers over 113,201 lines, plus 3,296 labelled training lines.
|
|
|
|
| 173 |
- **Labels:** the majority vote of a committee of three large language models.
|
| 174 |
-
- **Lines:** our own game dialogue, Project Gutenberg dialogue, GoEmotions (Apache-2.0), Civil Comments (CC0), and lines written for this project by Qwen3.8-27B.
|
| 175 |
-
- **Calibration:** the hostility threshold was matched on a player-speech dev set; the act, emotion and
|
| 176 |
- **The 16-bit file:** full precision's graph with every weight tensor of 1,024 values or more stored in 16 bits and turned back into 32 bits by the graph itself, so the same runtime and calibration serve both files.
|
| 177 |
|
| 178 |
## Use and limits
|
| 179 |
|
| 180 |
- **Use it** for character reactions and turn-taking in multi-character conversations: faces, feeling meters, moods, and who speaks next. **Do not use it** for moderation, safety decisions or judging real people.
|
| 181 |
- **English only.** It reads one previous line.
|
| 182 |
-
- **It misses
|
| 183 |
-
- **
|
| 184 |
-
- **The move on lines between characters is weaker than in rc2** (0.633 against 0.754).
|
| 185 |
- **It has only been tested in game settings.** Its test lines are committee-labelled generated and game dialogue, not logs from real players or from other multi-character voice experiences.
|
| 186 |
|
| 187 |
-
The numbers come from the training repository's report for this release, `2026-10-13-linecls-
|
|
|
|
| 13 |
- multi-party-conversation
|
| 14 |
- emotion-recognition
|
| 15 |
- dialogue-acts
|
| 16 |
+
- sarcasm-detection
|
| 17 |
- turn-taking
|
| 18 |
- real-time
|
| 19 |
- pipecat
|
|
|
|
| 21 |
- dialogue
|
| 22 |
---
|
| 23 |
|
| 24 |
+
# SpellSpeak Tone · RC4
|
| 25 |
|
| 26 |
When several AI characters share one conversation, as in a game, an interactive story or any other multi-character voice experience, each of them needs a structured reading of every line: how the speaker sounds, what the line does to each person present, and whether it leaves a question open for someone else to answer. The language model voicing a character could produce that reading, but only through extra output tokens, training or tool calls. That adds latency to every turn, takes capacity from the dialogue, risks malformed output, and repeats the work for each character in the scene.
|
| 27 |
|
| 28 |
+
SpellSpeak Tone treats the reading as multi-task classification by a separate small model. One pass over a line predicts the speaker's emotion, the social act directed at each person present with a hostility probability per person, the move the line makes in the conversation (does it ask, answer, pass on a rumour, say it does not know, hold back, or close the exchange), and whether the line is sarcastic. The application uses the tags for faces, feelings and who speaks next. Its constraints are millisecond inference on a CPU, robustness to unstructured user input, and rare false alarms.
|
| 29 |
|
| 30 |
[Examples and source](https://github.com/spellspeak/models/tree/main/tone)
|
| 31 |
|
| 32 |
+
## What is new in RC4
|
| 33 |
|
| 34 |
+
- **A sarcasm output, with its probability.** Every line now also says whether it is sarcastic: `sarcasm: {sarcastic, probability}`. Sarcastic means the line means something other than, usually the opposite of, what its words say: mock thanks, mock praise, mock agreement, ironic understatement, whether hostile, friendly or about the situation. On game dialogue in settings it never saw, it finds 73% of sarcastic lines and is right 86% of the time it says so (F1 0.79; two frontier labellers agree with each other at 0.91). It calls 1.6% of sincere everyday lines sarcastic.
|
| 35 |
+
- **The probability is calibrated** (error 0.03), so you can show it, or use your own threshold instead of the model's.
|
| 36 |
+
- **Everything else is rc3's.** Every rc3 tag, intensity, confidence and move is identical on 3,754 test lines. The sarcasm head reads the same pass: the same speed, 0.6 MB more.
|
|
|
|
| 37 |
|
| 38 |
| | |
|
| 39 |
|---|---|
|
| 40 |
+
| **Released** | 2026-10-13 · rc4 (package `spellspeak-tone-rc4.1`) |
|
| 41 |
+
| **What it does** | Reads one line of dialogue and tags the speaker's emotion, the social act toward each person present, the line's move in the conversation, and whether it is sarcastic |
|
| 42 |
+
| **What changed from rc3** | A small sarcasm head on rc3's encoder, in the same pass. rc3's outputs are unchanged. Contract `line-tags-0.3`: one optional field per line, so code written for rc3 runs unchanged |
|
| 43 |
+
| **Size** | 27.9M parameters in the graph (rc3's and 0.15M for the sarcasm head) · 111.7 MB ONNX in full precision (fp32), 56.0 MB with the weights in 16 bits (`model_fp16.onnx`) |
|
| 44 |
+
| **Speed** | p95 per line on 4 CPU threads (Apple M1 Max): 9.7 ms for a scene of 8 people, rc3 9.6 ms timed side by side. On 1 thread: 28.0 ms. The 16-bit file takes about the same time |
|
| 45 |
| **Runtime** | ONNX Runtime (1.24 or later) and tokenizers, on CPU. A Python reference runtime ships with it |
|
| 46 |
| **Files** | `model.opt.onnx` (the graph to run), `model.onnx` (the same graph unoptimized), `model_fp16.onnx` (the graph to run with its weights in 16 bits), `tokenizer.json` and `config.json`, with the reference runtime (`runtime/`), `example.py` and `requirements.txt`. `MANIFEST.json` lists every file with its size and sha256 |
|
| 47 |
| **Licence** | Apache-2.0 |
|
|
|
|
| 50 |
|
| 51 |
```bash
|
| 52 |
pip install -U huggingface_hub
|
| 53 |
+
hf download spellspeak/tone --revision rc4 --local-dir spellspeak-tone
|
| 54 |
cd spellspeak-tone
|
| 55 |
pip install -r requirements.txt
|
| 56 |
python example.py
|
| 57 |
```
|
| 58 |
|
| 59 |
+
The example puts three characters and the player in a room. The player asks everyone a question, the characters answer in different ways, and three more lines follow:
|
| 60 |
|
| 61 |
```
|
| 62 |
player to everyone: 'Who knows where the mill key is?'
|
|
|
|
| 73 |
move closes (0.98) · emotion happy · acts: Mara thanks
|
| 74 |
player to Tomas: "Touch my horse again and you'll regret it."
|
| 75 |
move other (0.71) · emotion angry · acts: Tomas threat
|
| 76 |
+
Wren to player: 'Oh, brilliant. You lost the key. Again.'
|
| 77 |
+
move other (0.72) · emotion contemptuous · acts: player insult · sarcastic (0.97)
|
| 78 |
```
|
| 79 |
|
| 80 |
The small download leaves out the two full-precision graphs and runs the 16-bit file. It prints the same tags:
|
| 81 |
|
| 82 |
```bash
|
| 83 |
+
hf download spellspeak/tone --revision rc4 --local-dir spellspeak-tone-small --exclude model.onnx --exclude model.opt.onnx
|
| 84 |
cd spellspeak-tone-small
|
| 85 |
pip install -r requirements.txt
|
| 86 |
python example.py --fp16
|
|
|
|
| 99 |
# tone = LineClassifier(".", precision="fp16") # the 16-bit file (model_fp16.onnx), for the small download
|
| 100 |
tags = tone.tag(LineInput(speaker="player", speaker_kind="player", to="Mara", targets=["Mara", "Tomas"],
|
| 101 |
text="oh great, thanks a lot", previous="Mara: Sorry, we're out of rooms tonight."))
|
| 102 |
+
print(tags.emotion, tags.acts["Mara"].act, tags.sarcasm) # sarcastic=True with its probability, after bad news
|
| 103 |
```
|
| 104 |
|
| 105 |
## Input and output
|
| 106 |
|
| 107 |
- **Input:** the line, the line before it, who speaks (the player or a character), who it is said to (or the room), and everyone present (tested up to 8 people; 160 tokens at most). Who it is said to and the line before are optional, and help: see the table below. Sarcasm is mostly read from the line before.
|
| 108 |
+
- **Per line:** an emotion (neutral, happy, amused, angry, sad, afraid, surprised, disgusted, contemptuous) with intensity (low, medium, high) and a confidence. Whether the line is only a listening sound such as "mm-hm" (backchannel). The **move**, with a confidence. **Sarcasm**: `sarcastic` (true or false) and `probability`, the calibrated chance the line is sarcastic.
|
| 109 |
- **Per person:** an act (insult, threat, accusation, tease, demand, dismiss, refusal, request, thanks, praise, apology, comfort, warning, none) with intensity and a confidence.
|
| 110 |
- **The moves.** asks: wants a reply or an action. answers: gives what was asked for, or offers information. hedges: offers something second-hand or unsure. doesnt_know: says it does not know. withholds: declines, deflects or dodges. closes: winds the exchange down and wants no reply. other: anything else.
|
| 111 |
- **Hostile** means insult, threat, accusation, or demand at high intensity. A tag is hostile when the hostility score reaches 0.675.
|
| 112 |
+
- **Sarcastic** is decided at the model's threshold, set so that at most 2% of sincere everyday lines are called sarcastic. The act and the emotion already say how hostile a sarcastic line is.
|
| 113 |
- **Confidence** is the chance the tag is right (calibrated). For a hostile tag it is the chance the line is hostile toward that person.
|
| 114 |
|
| 115 |
## Performance (held-out test sets, committee labels)
|
| 116 |
|
| 117 |
+
### Sarcasm
|
| 118 |
|
| 119 |
+
900 lines of game dialogue in six settings that no training line uses (263 sarcastic: sarcasm in twelve kinds, sincere look-alikes with the same words, and a speaker's two lines in a row), and 1,781 everyday lines (900 lines of natural player speech and 881 of game dialogue, 34 of them sarcastic):
|
|
|
|
|
|
|
|
|
|
|
|
|
| 120 |
|
| 121 |
+
| Measure | rc4 |
|
| 122 |
+
|---|---|
|
| 123 |
+
| F1, game dialogue in unseen settings (900 lines) | 0.793 (0.755 to 0.827; precision 0.862, recall 0.734) |
|
| 124 |
+
| Sincere everyday lines called sarcastic (1,747) | 1.55% (1.1% to 2.2%) |
|
| 125 |
+
| Calibration error of the probability (2,681 lines) | 0.028 |
|
| 126 |
+
|
| 127 |
+
For scale, two frontier labellers agree with each other at 0.905 F1 on the same 900 lines. It finds sarcasm after a setback ("Oh, wonderful. Another storm.") and about the situation best, friendly teasing sarcasm least.
|
| 128 |
+
|
| 129 |
+
On sitcom and Reddit sarcasm, labelled by what each writer meant, it is precise but finds little: 4% of the sitcom lines and 9% of the Reddit lines their sources call sarcastic, with 0.7% and 3.4% of their sincere lines called sarcastic.
|
| 130 |
|
| 131 |
+
### The emotion, the acts and the move
|
| 132 |
|
| 133 |
+
The same as rc3, tag for tag:
|
| 134 |
+
|
| 135 |
+
| Measure | rc4 (and rc3) |
|
| 136 |
+
|---|---|
|
| 137 |
+
| Act macro-F1, main test (1,905 lines) | 0.661 (0.630 to 0.689) |
|
| 138 |
+
| Emotion macro-F1, main test | 0.649 |
|
| 139 |
+
| Natural player speech (900 lines): act F1, emotion F1 | 0.781, 0.687 |
|
| 140 |
+
| Game settings never seen in training (900 lines): act F1 | 0.771 |
|
| 141 |
+
| Harmless lines called hostile: prose-style, player speech | 1.4%, 2.3% |
|
| 142 |
+
| Hostile lines caught: main test, player speech | 76%, 84% |
|
| 143 |
+
| Sarcastic lines hostile toward someone, caught (141 pairs in unseen settings) | 73% |
|
| 144 |
+
| Move macro-F1 (824 lines) | 0.816 (0.785 to 0.845) |
|
| 145 |
+
| Answered or still open (454 lines) | 83.0% |
|
| 146 |
+
| Asks or not (824 lines) | 96.0% |
|
| 147 |
+
| Calibration error: act tags, emotion, move | 0.07, 0.05, 0.05 |
|
| 148 |
|
| 149 |
For scale, two frontier labellers agree with each other at 0.766 act F1, 0.709 emotion F1 and 0.845 move F1.
|
| 150 |
|
|
|
|
| 154 |
|
| 155 |
| Measure | As given | Without `to` | Without the line before | Without either |
|
| 156 |
|---|---|---|---|---|
|
| 157 |
+
| Sarcasm F1, unseen settings | 0.793 | 0.790 | 0.472 | 0.426 |
|
| 158 |
+
| Sincere everyday lines called sarcastic | 1.55% | 1.20% | 0.29% | 0.23% |
|
| 159 |
| Act macro-F1, main test | 0.661 | 0.564 | 0.656 | 0.552 |
|
| 160 |
| Emotion macro-F1, main test | 0.649 | 0.649 | 0.640 | 0.636 |
|
| 161 |
| Harmless lines called hostile, prose | 1.4% | 1.5% | 1.3% | 1.8% |
|
|
|
|
| 164 |
| Move macro-F1 | 0.816 | 0.805 | 0.722 | 0.683 |
|
| 165 |
| Answered or still open | 83.0% | 80.0% | 66.1% | 60.1% |
|
| 166 |
|
| 167 |
+
- **Send the line before.** Without it, sarcasm is found less than half as often (it stays precise), and the move is weaker.
|
| 168 |
- **Send who a line is said to when you know it.** Without it, more harmless player lines are called hostile.
|
|
|
|
| 169 |
|
| 170 |
### The 16-bit file
|
| 171 |
|
| 172 |
+
The same 5,204 test lines, and the 2,681 sarcasm test lines, tagged by `model_fp16.onnx` and by full precision, in the same four columns:
|
| 173 |
|
| 174 |
| | As given | Without `to` | Without the line before | Without either |
|
| 175 |
|---|---|---|---|---|
|
| 176 |
| Lines with every tag the same as full precision | 100% | 99.98% (one act toward one person differs) | 100% | 99.96% (two lines differ) |
|
| 177 |
+
| The sarcasm decision the same | 100% | 100% | 100% | 100% |
|
| 178 |
|
| 179 |
+
Every measure in the tables above is the same with the 16-bit file. It computes in 32 bits, so it saves the download and the disk, not time or memory (247 MB resident against 201 MB).
|
| 180 |
|
| 181 |
## How it was made
|
| 182 |
|
| 183 |
+
- **Model:** MiniLM-L6 (`sentence-transformers/all-MiniLM-L6-v2` at 1110a24, Apache-2.0) with six small heads. For the move, trainable copies of its top two layers read its fourth layer's output, and a small head reads them. The sarcasm head (148,610 parameters) reads the encoder's last layer, which stays as rc3 trained it.
|
| 184 |
- **Distillation:** the emotion and the acts from three Ettin-1B teachers (`jhu-clsp/ettin-encoder-1b`, MIT) over 374,534 lines, plus 20,286 lines labelled directly. The move from rc2's three move teachers over 113,201 lines, plus 3,296 labelled training lines.
|
| 185 |
+
- **Sarcasm:** trained on 4,000 lines labelled by the committee (1,137 sarcastic): our game dialogue's sarcasm in twelve kinds with its sincere look-alikes, a speaker's two lines in a row, and everyday lines. The threshold was chosen on dev lines, and its probability curve fitted on them.
|
| 186 |
- **Labels:** the majority vote of a committee of three large language models.
|
| 187 |
+
- **Lines:** our own game dialogue, Project Gutenberg dialogue, GoEmotions (Apache-2.0), Civil Comments (CC0), and lines written for this project by Qwen3.8-27B. Sitcom and Reddit sarcasm was used only as a test.
|
| 188 |
+
- **Calibration:** the hostility threshold was matched on a player-speech dev set; the act, emotion, move and sarcasm curves were fitted on dev.
|
| 189 |
- **The 16-bit file:** full precision's graph with every weight tensor of 1,024 values or more stored in 16 bits and turned back into 32 bits by the graph itself, so the same runtime and calibration serve both files.
|
| 190 |
|
| 191 |
## Use and limits
|
| 192 |
|
| 193 |
- **Use it** for character reactions and turn-taking in multi-character conversations: faces, feeling meters, moods, and who speaks next. **Do not use it** for moderation, safety decisions or judging real people.
|
| 194 |
- **English only.** It reads one previous line.
|
| 195 |
+
- **It misses sarcasm.** It misses about one sarcastic line in four in game dialogue, most friendly teasing sarcasm, and most sitcom and Reddit sarcasm. Without the line before, it finds less than half as much.
|
| 196 |
+
- **It misses other things.** It misses about one in four hostile sarcastic lines, and gets "answered or still open" wrong about one time in six.
|
| 197 |
+
- **Rare emotions are weaker than in rc2**, disgust and fear most. **The move on lines between characters is weaker than in rc2** (0.633 against 0.754).
|
| 198 |
- **It has only been tested in game settings.** Its test lines are committee-labelled generated and game dialogue, not logs from real players or from other multi-character voice experiences.
|
| 199 |
|
| 200 |
+
The numbers come from the training repository's report for this release, `2026-10-13-linecls-r10-sarcasm-output.md` (at commit 2cf51c7). The full-precision files were packaged from git tag `spellspeak-tone-rc4` (commit 78d74e8), and this release was packaged from git tag `spellspeak-tone-rc4.1`. That repository is private.
|
config.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
| 1 |
{
|
| 2 |
-
"run_id": "linecls-
|
| 3 |
"encoder": "minilm-l6",
|
| 4 |
"repo": "sentence-transformers/all-MiniLM-L6-v2",
|
| 5 |
"revision": "1110a243fdf4706b3f48f1d95db1a4f5529b4d41",
|
|
@@ -60,9 +60,14 @@
|
|
| 60 |
"a": 1.063282,
|
| 61 |
"b": 0.528266,
|
| 62 |
"items": 680
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 63 |
}
|
| 64 |
},
|
| 65 |
-
"calibration_note": "Platt curves fitted on dev (exp-237); a confidence is the chance the tag is right; r8: the exchange curve fitted on the exchange dev lines (exp-247)",
|
| 66 |
"base_run_id": "linecls-r2-student-minilm-l6-r9bs1-seed1",
|
| 67 |
"exchanges": [
|
| 68 |
"asks",
|
|
@@ -80,5 +85,22 @@
|
|
| 80 |
"seed": 0,
|
| 81 |
"params": 4290823
|
| 82 |
},
|
| 83 |
-
"contract": "line-tags-0.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 84 |
}
|
|
|
|
| 1 |
{
|
| 2 |
+
"run_id": "linecls-r10-sarcasm",
|
| 3 |
"encoder": "minilm-l6",
|
| 4 |
"repo": "sentence-transformers/all-MiniLM-L6-v2",
|
| 5 |
"revision": "1110a243fdf4706b3f48f1d95db1a4f5529b4d41",
|
|
|
|
| 60 |
"a": 1.063282,
|
| 61 |
"b": 0.528266,
|
| 62 |
"items": 680
|
| 63 |
+
},
|
| 64 |
+
"sarcasm": {
|
| 65 |
+
"a": 1.126858,
|
| 66 |
+
"b": -0.9161,
|
| 67 |
+
"items": 1857
|
| 68 |
}
|
| 69 |
},
|
| 70 |
+
"calibration_note": "Platt curves fitted on dev (exp-237); a confidence is the chance the tag is right; r8: the exchange curve fitted on the exchange dev lines (exp-247); r10: the sarcasm curve fitted on the sarcasm dev lines (exp-274)",
|
| 71 |
"base_run_id": "linecls-r2-student-minilm-l6-r9bs1-seed1",
|
| 72 |
"exchanges": [
|
| 73 |
"asks",
|
|
|
|
| 85 |
"seed": 0,
|
| 86 |
"params": 4290823
|
| 87 |
},
|
| 88 |
+
"contract": "line-tags-0.3",
|
| 89 |
+
"exchange_run_id": "linecls-r9-exchange",
|
| 90 |
+
"sarcasm": {
|
| 91 |
+
"tau_raw": 0.69,
|
| 92 |
+
"classes": [
|
| 93 |
+
"sincere",
|
| 94 |
+
"sarcastic"
|
| 95 |
+
],
|
| 96 |
+
"head": {
|
| 97 |
+
"variant": "mean",
|
| 98 |
+
"kind": "mean",
|
| 99 |
+
"feature": null,
|
| 100 |
+
"branch": null,
|
| 101 |
+
"branch_kind": null,
|
| 102 |
+
"seed": 0,
|
| 103 |
+
"params": 148610
|
| 104 |
+
}
|
| 105 |
+
}
|
| 106 |
}
|
example.py
CHANGED
|
@@ -5,7 +5,7 @@
|
|
| 5 |
python example.py --fp16 # the 16-bit file: model_fp16.onnx, half the size
|
| 6 |
|
| 7 |
The model files are read from this folder. Without them, as in a git checkout, they are downloaded once from Hugging
|
| 8 |
-
Face (spellspeak/tone, release
|
| 9 |
"""
|
| 10 |
import pathlib
|
| 11 |
import sys
|
|
@@ -23,7 +23,7 @@ model_dir = HERE
|
|
| 23 |
if not all((HERE / f).exists() for f in MODEL_FILES):
|
| 24 |
from huggingface_hub import snapshot_download
|
| 25 |
|
| 26 |
-
model_dir = snapshot_download("spellspeak/tone", revision="
|
| 27 |
tone = LineClassifier(model_dir, threads=4, precision=PRECISION)
|
| 28 |
|
| 29 |
# Three characters and the player. Each line names who speaks, who it is to (None: the room) and the line before it.
|
|
@@ -37,6 +37,7 @@ LINES = [
|
|
| 37 |
("Tomas", "player", "No idea, sorry.", QUESTION),
|
| 38 |
("player", "Mara", "thanks, that's all i needed", "Mara: It's under the counter, love."),
|
| 39 |
("player", "Tomas", "Touch my horse again and you'll regret it.", None),
|
|
|
|
| 40 |
]
|
| 41 |
|
| 42 |
for speaker, to, text, previous in LINES:
|
|
@@ -45,9 +46,11 @@ for speaker, to, text, previous in LINES:
|
|
| 45 |
tags = tone.tag(line)
|
| 46 |
acts = ", ".join(f"{who} {a.act}" for who, a in tags.acts.items() if a.act != "none") or "none"
|
| 47 |
move = getattr(tags, "exchange", None) # models from rc2 on
|
|
|
|
|
|
|
| 48 |
print(f"{speaker} to {to or 'everyone'}: {text!r}")
|
| 49 |
-
print(f" move {move.label} ({move.confidence:.2f}) · emotion {tags.emotion} · acts: {acts}" if move else
|
| 50 |
-
f" emotion {tags.emotion} · acts: {acts}")
|
| 51 |
|
| 52 |
# The full output for one line, as JSON
|
| 53 |
print(tone.tag(LineInput(speaker="Wren", speaker_kind="npc", to="Tomas", targets=["Tomas", "Mara", "player"],
|
|
|
|
| 5 |
python example.py --fp16 # the 16-bit file: model_fp16.onnx, half the size
|
| 6 |
|
| 7 |
The model files are read from this folder. Without them, as in a git checkout, they are downloaded once from Hugging
|
| 8 |
+
Face (spellspeak/tone, release rc4). With --fp16, only model_fp16.onnx, tokenizer.json and config.json are needed.
|
| 9 |
"""
|
| 10 |
import pathlib
|
| 11 |
import sys
|
|
|
|
| 23 |
if not all((HERE / f).exists() for f in MODEL_FILES):
|
| 24 |
from huggingface_hub import snapshot_download
|
| 25 |
|
| 26 |
+
model_dir = snapshot_download("spellspeak/tone", revision="rc4", allow_patterns=MODEL_FILES)
|
| 27 |
tone = LineClassifier(model_dir, threads=4, precision=PRECISION)
|
| 28 |
|
| 29 |
# Three characters and the player. Each line names who speaks, who it is to (None: the room) and the line before it.
|
|
|
|
| 37 |
("Tomas", "player", "No idea, sorry.", QUESTION),
|
| 38 |
("player", "Mara", "thanks, that's all i needed", "Mara: It's under the counter, love."),
|
| 39 |
("player", "Tomas", "Touch my horse again and you'll regret it.", None),
|
| 40 |
+
("Wren", "player", "Oh, brilliant. You lost the key. Again.", "player: I think I dropped the mill key in the river."),
|
| 41 |
]
|
| 42 |
|
| 43 |
for speaker, to, text, previous in LINES:
|
|
|
|
| 46 |
tags = tone.tag(line)
|
| 47 |
acts = ", ".join(f"{who} {a.act}" for who, a in tags.acts.items() if a.act != "none") or "none"
|
| 48 |
move = getattr(tags, "exchange", None) # models from rc2 on
|
| 49 |
+
sarcasm = getattr(tags, "sarcasm", None) # models from rc4 on
|
| 50 |
+
said = f" · sarcastic ({sarcasm.probability:.2f})" if sarcasm and sarcasm.sarcastic else ""
|
| 51 |
print(f"{speaker} to {to or 'everyone'}: {text!r}")
|
| 52 |
+
print(f" move {move.label} ({move.confidence:.2f}) · emotion {tags.emotion} · acts: {acts}{said}" if move else
|
| 53 |
+
f" emotion {tags.emotion} · acts: {acts}{said}")
|
| 54 |
|
| 55 |
# The full output for one line, as JSON
|
| 56 |
print(tone.tag(LineInput(speaker="Wren", speaker_kind="npc", to="Tomas", targets=["Tomas", "Mara", "player"],
|
model.onnx
CHANGED
|
@@ -1,3 +1,3 @@
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
-
oid sha256:
|
| 3 |
-
size
|
|
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:0e4c32cae657161a27b2a1b486f1f78781351cbb6745d78bea4c1a7e7e7dbe3d
|
| 3 |
+
size 111766436
|
model.opt.onnx
CHANGED
|
@@ -1,3 +1,3 @@
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
-
oid sha256:
|
| 3 |
-
size
|
|
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:727035a576887a119e60dae1d6271b1f23b1f3fddf386cbd45b8cb38a9494730
|
| 3 |
+
size 111727815
|
model_fp16.onnx
CHANGED
|
@@ -1,3 +1,3 @@
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
-
oid sha256:
|
| 3 |
-
size
|
|
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:7d59948b90d24614da8b95a9e09c080d50e60feb32b7ecbbad3c22c537eb91a1
|
| 3 |
+
size 55995133
|
runtime/contracts/schemas/line_tags.py
CHANGED
|
@@ -190,3 +190,43 @@ def load_examples_02() -> list[LineTagRecord02]:
|
|
| 190 |
import yaml
|
| 191 |
|
| 192 |
return [LineTagRecord02.model_validate(r) for r in yaml.safe_load(EXAMPLES_02_PATH.read_text())]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 190 |
import yaml
|
| 191 |
|
| 192 |
return [LineTagRecord02.model_validate(r) for r in yaml.safe_load(EXAMPLES_02_PATH.read_text())]
|
| 193 |
+
|
| 194 |
+
|
| 195 |
+
# --- line-tags-0.3: the sarcasm output (rc4, revision r10) -----------------------------------------------------------
|
| 196 |
+
|
| 197 |
+
SCHEMA_VERSION_03 = "line-tags-0.3"
|
| 198 |
+
|
| 199 |
+
|
| 200 |
+
class SarcasmTag(Strict):
|
| 201 |
+
"""Whether a line is sarcastic, read with the line before it (guide 0.3): it means something other than, usually the
|
| 202 |
+
opposite of, what its words say. How hostile it is stays with the act and the emotion."""
|
| 203 |
+
|
| 204 |
+
sarcastic: bool
|
| 205 |
+
probability: Probability | None = None # the calibrated chance the line is sarcastic; labels may carry none
|
| 206 |
+
|
| 207 |
+
|
| 208 |
+
class LineTags03(LineTags02):
|
| 209 |
+
"""What the classifier returns for one line under line-tags-0.3: the 0.2 tags plus sarcasm, when the model has the
|
| 210 |
+
sarcasm head."""
|
| 211 |
+
|
| 212 |
+
sarcasm: SarcasmTag | None = None
|
| 213 |
+
|
| 214 |
+
|
| 215 |
+
class LineTagRecord03(LineTagRecord02):
|
| 216 |
+
"""A labelled line under line-tags-0.3. Every 0.1 and 0.2 record validates here unchanged; a record that carries a
|
| 217 |
+
sarcasm label says line-tags-0.3."""
|
| 218 |
+
|
| 219 |
+
schema_version: Literal["line-tags-0.1", "line-tags-0.2", "line-tags-0.3"] = SCHEMA_VERSION_03
|
| 220 |
+
tags: LineTags03
|
| 221 |
+
|
| 222 |
+
@model_validator(mode="after")
|
| 223 |
+
def _exchange_needs_02(self) -> "LineTagRecord03": # overrides 0.2's check: 0.3 carries the move too
|
| 224 |
+
if self.tags.exchange is not None and self.schema_version not in (SCHEMA_VERSION_02, SCHEMA_VERSION_03):
|
| 225 |
+
raise ValueError("`exchange` is a line-tags-0.2 field: the record must say line-tags-0.2 or later")
|
| 226 |
+
return self
|
| 227 |
+
|
| 228 |
+
@model_validator(mode="after")
|
| 229 |
+
def _sarcasm_needs_03(self) -> "LineTagRecord03":
|
| 230 |
+
if self.tags.sarcasm is not None and self.schema_version != SCHEMA_VERSION_03:
|
| 231 |
+
raise ValueError("`sarcasm` is a line-tags-0.3 field: the record must say schema_version line-tags-0.3")
|
| 232 |
+
return self
|
runtime/harness/expression/calibrate.py
CHANGED
|
@@ -49,6 +49,13 @@ def exchange_confidence(raw: float, cal: dict | None) -> float:
|
|
| 49 |
return platt(raw, cal["exchange"])
|
| 50 |
|
| 51 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 52 |
def fit_platt(p, y, l2: float = 1e-3, iters: int = 100) -> dict:
|
| 53 |
"""Two parameters by Newton's method on the log loss of y (0 or 1) given logit(p); a little L2 for stability."""
|
| 54 |
import numpy as np
|
|
|
|
| 49 |
return platt(raw, cal["exchange"])
|
| 50 |
|
| 51 |
|
| 52 |
+
def sarcasm_probability(raw: float, cal: dict | None) -> float:
|
| 53 |
+
"""r10: the chance the line is sarcastic; the raw probability when the model has no sarcasm curve."""
|
| 54 |
+
if not cal or "sarcasm" not in cal:
|
| 55 |
+
return raw
|
| 56 |
+
return platt(raw, cal["sarcasm"])
|
| 57 |
+
|
| 58 |
+
|
| 59 |
def fit_platt(p, y, l2: float = 1e-3, iters: int = 100) -> dict:
|
| 60 |
"""Two parameters by Newton's method on the log loss of y (0 or 1) given logit(p); a little L2 for stability."""
|
| 61 |
import numpy as np
|
runtime/harness/expression/classifier.py
CHANGED
|
@@ -12,6 +12,10 @@ r8 (line-tags-0.2): a model whose graph has an `exchange` output and whose confi
|
|
| 12 |
line's exchange move (asks, answers, hedges, doesnt_know, withholds, closes, other) with its confidence, as
|
| 13 |
`LineTags02`. A model without it (RC1) returns `LineTags` exactly as before.
|
| 14 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
Precision: full precision ("fp32") is the default and the reference. From release rc2.1 a folder may also hold
|
| 16 |
`model_fp16.onnx`, the same graph with its weights stored in 16 bits (half the size, computed in 32 bits), picked with
|
| 17 |
`precision="fp16"`.
|
|
@@ -22,8 +26,8 @@ import json
|
|
| 22 |
import pathlib
|
| 23 |
import time
|
| 24 |
|
| 25 |
-
from contracts.schemas.line_tags import ActTag, ExchangeTag, LineInput, LineTags, LineTags02, is_hostile
|
| 26 |
-
from harness.expression.calibrate import emotion_confidence, exchange_confidence, tag_confidence
|
| 27 |
from harness.expression.decide import decide, summed_hostility
|
| 28 |
from harness.expression.render import render, span_mask
|
| 29 |
|
|
@@ -68,6 +72,9 @@ class LineClassifier:
|
|
| 68 |
# r8: the exchange head, when the graph has it; RC1's graph does not
|
| 69 |
self.exchanges = self.config.get("exchanges") if "exchange" in outputs else None
|
| 70 |
self._exchange_at = outputs.index("exchange") if self.exchanges else None
|
|
|
|
|
|
|
|
|
|
| 71 |
|
| 72 |
def logits(self, inp: LineInput):
|
| 73 |
"""Raw outputs for one line: emotion, emotion intensity, act per person, act intensity per person, backchannel."""
|
|
@@ -93,10 +100,10 @@ class LineClassifier:
|
|
| 93 |
pe, pei, pa, pai = softmax(le[0]), softmax(lei[0]), softmax(la), softmax(lai)
|
| 94 |
emo = self.emotions[int(pe.argmax())]
|
| 95 |
if float(softmax(lb[0])[1]) >= 0.5: # a backchannel is not a turn: no act toward anyone
|
| 96 |
-
return self._with_exchange(LineTags(
|
| 97 |
emotion=emo, emotion_intensity="low" if emo == "neutral" else self.intensities[int(pei.argmax())],
|
| 98 |
emotion_confidence=round(emotion_confidence(float(pe.max()), self.calibration), 4),
|
| 99 |
-
acts={t: ActTag(act="none") for t in inp.targets}, backchannel=True), out)
|
| 100 |
acts = {}
|
| 101 |
for i, t in enumerate(inp.targets):
|
| 102 |
ph = float(p_host[i]) if p_host is not None else summed_hostility(pa[i])
|
|
@@ -106,9 +113,9 @@ class LineClassifier:
|
|
| 106 |
hostile = is_hostile(act, inten)
|
| 107 |
conf = ph if hostile else float(pa[i][self.acts.index(act)])
|
| 108 |
acts[t] = ActTag(act=act, intensity=inten, confidence=round(tag_confidence(conf, hostile, self.calibration), 4))
|
| 109 |
-
return self._with_exchange(LineTags(
|
| 110 |
emotion=emo, emotion_intensity="low" if emo == "neutral" else self.intensities[int(pei.argmax())],
|
| 111 |
-
emotion_confidence=round(emotion_confidence(float(pe.max()), self.calibration), 4), acts=acts, backchannel=False), out)
|
| 112 |
|
| 113 |
def _with_exchange(self, tags: LineTags, out) -> LineTags:
|
| 114 |
"""RC1 and any model without the exchange head: the tags unchanged. r8 on: the same tags plus the exchange move."""
|
|
@@ -124,6 +131,20 @@ class LineClassifier:
|
|
| 124 |
return LineTags02(**{k: getattr(tags, k) for k in type(tags).model_fields},
|
| 125 |
exchange=ExchangeTag(label=self.exchanges[i], confidence=conf))
|
| 126 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 127 |
def timed(self, inp: LineInput) -> tuple[LineTags, float]:
|
| 128 |
t = time.perf_counter()
|
| 129 |
out = self.tag(inp)
|
|
|
|
| 12 |
line's exchange move (asks, answers, hedges, doesnt_know, withholds, closes, other) with its confidence, as
|
| 13 |
`LineTags02`. A model without it (RC1) returns `LineTags` exactly as before.
|
| 14 |
|
| 15 |
+
r10 (line-tags-0.3): a model whose graph also has a `sarcasm` output returns `sarcasm: {sarcastic, probability}` as
|
| 16 |
+
`LineTags03`: the calibrated chance the line is sarcastic, and the model's decision at its own threshold (the config's
|
| 17 |
+
`sarcasm.tau_raw` on the raw probability). Models without it return what they always did.
|
| 18 |
+
|
| 19 |
Precision: full precision ("fp32") is the default and the reference. From release rc2.1 a folder may also hold
|
| 20 |
`model_fp16.onnx`, the same graph with its weights stored in 16 bits (half the size, computed in 32 bits), picked with
|
| 21 |
`precision="fp16"`.
|
|
|
|
| 26 |
import pathlib
|
| 27 |
import time
|
| 28 |
|
| 29 |
+
from contracts.schemas.line_tags import ActTag, ExchangeTag, LineInput, LineTags, LineTags02, LineTags03, SarcasmTag, is_hostile
|
| 30 |
+
from harness.expression.calibrate import emotion_confidence, exchange_confidence, sarcasm_probability, tag_confidence
|
| 31 |
from harness.expression.decide import decide, summed_hostility
|
| 32 |
from harness.expression.render import render, span_mask
|
| 33 |
|
|
|
|
| 72 |
# r8: the exchange head, when the graph has it; RC1's graph does not
|
| 73 |
self.exchanges = self.config.get("exchanges") if "exchange" in outputs else None
|
| 74 |
self._exchange_at = outputs.index("exchange") if self.exchanges else None
|
| 75 |
+
# r10: the sarcasm head, when the graph has it; rc3 and earlier do not
|
| 76 |
+
self._sarcasm_at = outputs.index("sarcasm") if "sarcasm" in outputs else None
|
| 77 |
+
self.sarcasm_tau_raw = float((self.config.get("sarcasm") or {}).get("tau_raw", 0.5))
|
| 78 |
|
| 79 |
def logits(self, inp: LineInput):
|
| 80 |
"""Raw outputs for one line: emotion, emotion intensity, act per person, act intensity per person, backchannel."""
|
|
|
|
| 100 |
pe, pei, pa, pai = softmax(le[0]), softmax(lei[0]), softmax(la), softmax(lai)
|
| 101 |
emo = self.emotions[int(pe.argmax())]
|
| 102 |
if float(softmax(lb[0])[1]) >= 0.5: # a backchannel is not a turn: no act toward anyone
|
| 103 |
+
return self._with_sarcasm(self._with_exchange(LineTags(
|
| 104 |
emotion=emo, emotion_intensity="low" if emo == "neutral" else self.intensities[int(pei.argmax())],
|
| 105 |
emotion_confidence=round(emotion_confidence(float(pe.max()), self.calibration), 4),
|
| 106 |
+
acts={t: ActTag(act="none") for t in inp.targets}, backchannel=True), out), out)
|
| 107 |
acts = {}
|
| 108 |
for i, t in enumerate(inp.targets):
|
| 109 |
ph = float(p_host[i]) if p_host is not None else summed_hostility(pa[i])
|
|
|
|
| 113 |
hostile = is_hostile(act, inten)
|
| 114 |
conf = ph if hostile else float(pa[i][self.acts.index(act)])
|
| 115 |
acts[t] = ActTag(act=act, intensity=inten, confidence=round(tag_confidence(conf, hostile, self.calibration), 4))
|
| 116 |
+
return self._with_sarcasm(self._with_exchange(LineTags(
|
| 117 |
emotion=emo, emotion_intensity="low" if emo == "neutral" else self.intensities[int(pei.argmax())],
|
| 118 |
+
emotion_confidence=round(emotion_confidence(float(pe.max()), self.calibration), 4), acts=acts, backchannel=False), out), out)
|
| 119 |
|
| 120 |
def _with_exchange(self, tags: LineTags, out) -> LineTags:
|
| 121 |
"""RC1 and any model without the exchange head: the tags unchanged. r8 on: the same tags plus the exchange move."""
|
|
|
|
| 131 |
return LineTags02(**{k: getattr(tags, k) for k in type(tags).model_fields},
|
| 132 |
exchange=ExchangeTag(label=self.exchanges[i], confidence=conf))
|
| 133 |
|
| 134 |
+
def _with_sarcasm(self, tags: LineTags, out) -> LineTags:
|
| 135 |
+
"""rc3 and earlier: the tags unchanged. r10 on: the same tags plus sarcasm, decided on the raw probability at the
|
| 136 |
+
model's threshold, reported as the calibrated probability."""
|
| 137 |
+
if self._sarcasm_at is None:
|
| 138 |
+
return tags
|
| 139 |
+
import numpy as np
|
| 140 |
+
|
| 141 |
+
z = np.asarray(out[self._sarcasm_at][0], dtype=np.float64)
|
| 142 |
+
p = np.exp(z - z.max())
|
| 143 |
+
raw = float(p[1] / p.sum())
|
| 144 |
+
return LineTags03(**{k: getattr(tags, k) for k in type(tags).model_fields},
|
| 145 |
+
sarcasm=SarcasmTag(sarcastic=raw >= self.sarcasm_tau_raw,
|
| 146 |
+
probability=round(sarcasm_probability(raw, self.calibration), 4)))
|
| 147 |
+
|
| 148 |
def timed(self, inp: LineInput) -> tuple[LineTags, float]:
|
| 149 |
t = time.perf_counter()
|
| 150 |
out = self.tag(inp)
|