spike(semif): SemIf as Cicada's mood source is slower and less apt (no service change)
Against talk /face's guided pose (first paragraph 246 ms median), SemIf in parallel adds 32 ms and SemIf-first adds 94 ms (n=72 each, noise floor 16.5 ms). Removing the pose header saves only ~31 ms, and SemIf shares GPU 1 with the LLM. Acceptable pose 67% vs 92% on clear-emotion lines, and the mood carried through mundane follow-ups 7/15 vs 14/15. SemIf gestures far less (13% vs 58%). README: rotations cost options^2 in suffix tokens, and /decide/shared returns 422 when an object state's last value ends in ) ; or }.
This commit is contained in:
@@ -97,3 +97,64 @@ said, run beside the LLM.
|
|||||||
its wording is fragile. Wyrd's v1 is met and no churn has been observed (2–6 nodes per real
|
its wording is fragile. Wyrd's v1 is met and no churn has been observed (2–6 nodes per real
|
||||||
campaign), so by the roadmap gate this is a parking-lot item with a trigger: build it if live
|
campaign), so by the roadmap gate this is a parking-lot item with a trigger: build it if live
|
||||||
play shows churn, after labelling ~50 real turns as a held-out set.
|
play shows churn, after labelling ~50 real turns as a held-out set.
|
||||||
|
|
||||||
|
## Follow-up: SemIf as Cicada's mood source — FASTER? WORKS? (Prime, 0937: "build nothing … test cicada on latency grounds with clear emotional context")
|
||||||
|
|
||||||
|
Parked design, henge id 88: SemIf replaces the chat model's guided pose. Each call sees the earlier
|
||||||
|
turns, each stamped with the pose SemIf chose, and the stamp drives the carry-over. Harness:
|
||||||
|
`services/semif-serve/spike/cicada_mood_latency.py` + `cicada_mood_analyze.py`, raw data in
|
||||||
|
`cicada-mood-2026-09-27/`. 24 emotionally clear single lines and three 5-turn arcs, 3 runs,
|
||||||
|
interleaved. The prompt and schema are imported from tts-stack talk `app.py`, so A matches /face
|
||||||
|
byte for byte. The chat model is char-rp-fast = G4-MeroMero-26B-A4B NVFP4A16 on fv-ml1:8021,
|
||||||
|
**on GPU 1, the same GPU as semif**.
|
||||||
|
|
||||||
|
**Answer: no on both counts.**
|
||||||
|
|
||||||
|
| (singles, n=72 each) | first paragraph ready, direction known | vs today |
|
||||||
|
|---|---|---|
|
||||||
|
| A: today, guided `{pose, gesture, text}` | **246 ms** (IQR 230–263); the pose closes at 86 ms | — |
|
||||||
|
| BA: SemIf and a text-only LLM call at once | 285 ms | **+32 ms** (95% CI +27..+41), faster in 10/72 |
|
||||||
|
| BP: SemIf first, mood in the prompt | 352 ms | **+94 ms** (CI +85..+117), faster in 0/72 |
|
||||||
|
|
||||||
|
Noise floor A vs A: median |diff| 16.5 ms. The arcs agree: BP is +98 ms.
|
||||||
|
- **Why it can't win on speed.** Dropping the pose+gesture header saves only ~31 ms (the LLM
|
||||||
|
alone, text-only, reaches first paragraph in 215 ms vs 246 ms). The prompt is prefix-cached, so
|
||||||
|
its 1,885-vs-909 tokens barely matter. SemIf's fast path costs 136 ms on its own. Run
|
||||||
|
concurrently, it slows the LLM's decode on the shared GPU: SemIf goes to 196 ms and the LLM's
|
||||||
|
first paragraph to 285 ms, and SemIf was the bottleneck in only 3/72 cases. DERIVED, not
|
||||||
|
measured: with SemIf on a different GPU, the async version would be about max(215, 136) =
|
||||||
|
~31 ms faster than today. That is the ceiling.
|
||||||
|
- **Rotations are out.** 16 options with rotations is 550–1,200 ms, so every live arm used one
|
||||||
|
ordering (pose + gesture ~110–136 ms).
|
||||||
|
|
||||||
|
| works (clear cases) | today (A) | SemIf (live config) |
|
||||||
|
|---|---|---|
|
||||||
|
| acceptable pose, singles | **66/72** (92%) | 48/72 (67%), 16/24 per case, deterministic |
|
||||||
|
| controls | 12/12 | 12/12 |
|
||||||
|
| egregious | 0 | 0 |
|
||||||
|
| arcs acceptable | **40/45** | 25/45 |
|
||||||
|
| mood carries through a mundane follow-up | **14/15** | **7/15** |
|
||||||
|
| distinct poses used | 10 | 9 (no collapse to neutral) |
|
||||||
|
| gesture rate, all / calm commands | 42/72 / 2/12 | **9/72** / 0/12 |
|
||||||
|
|
||||||
|
- SemIf's misses are systematic: hostility → annoyed, "someone's in the hallway" → curious,
|
||||||
|
"someone's trying the back door" → suspicious, jokes → curious or neutral.
|
||||||
|
- **The stamped previous mood did NOT carry.** In the vet arc, SemIf went concerned → neutral →
|
||||||
|
neutral in all 3 runs ("I don't want to talk about it", "play something quiet") while the model
|
||||||
|
held sad, sad, sad. The previous pose was in the state and it lost to the neutral-sounding line.
|
||||||
|
- SemIf's pick agrees with the model 43% of the time, against the model's own run-to-run
|
||||||
|
agreement of 83%, so this is a different judgment and not noise.
|
||||||
|
- Wording grid (deterministic, singles): authored 16/24, short 14/24, plain **19/24** but it
|
||||||
|
called "turn off the lights" sleepy. Rotations add about one case at 5–8× the cost. Null
|
||||||
|
control (content-free state) → "content" 24/24, i.e. 5/24 by base rate, so SemIf is reading
|
||||||
|
the evidence.
|
||||||
|
- No vocal-sound vs pose clashes in any arm (0/117, 0/72, 0/117; crude detector).
|
||||||
|
- The one bright spot matches the gate spike: **SemIf is far more sparing with gestures (13% vs
|
||||||
|
58%)**, and the model is well over its own "fewer than 1 in 3" target on emotionally loaded
|
||||||
|
lines.
|
||||||
|
- Limits: one labeller, 24 + 15 cases, one model size (4B vs 26B-A4B), wordings not tuned.
|
||||||
|
Tuning could lift accuracy some (plain 19/24), but it cannot create latency headroom that
|
||||||
|
isn't there.
|
||||||
|
- Found on the way (service, not fixed): `/decide/shared` 422s an object state whose last value
|
||||||
|
ends in `)`, `;` or `}`. Also, rotations over long option lists cost options², and hit one cold
|
||||||
|
503. Both are now in `stacks/semif/README.md`.
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Persistent memory — eshpfi-management
|
# Persistent memory — eshpfi-management
|
||||||
|
|
||||||
_Last updated: 2026-09-27 ~0920 PT (SemIf consumer-fit spikes done: Cicada affect gate + Wyrd scene change, two rulings with Prime; semif-serve 0.1.3 live; overnight backups all green.)_
|
_Last updated: 2026-09-27 ~0947 PT (SemIf spikes done and ruled build-nothing; SemIf-as-Cicada-mood measured slower and worse; semif-serve 0.1.3 live; overnight backups all green.)_
|
||||||
|
|
||||||
> **Always check for `/tmp/infra-ops-handoff.md`** — if it exists and its
|
> **Always check for `/tmp/infra-ops-handoff.md`** — if it exists and its
|
||||||
> `Written:` stamp is under **8 hours** old, read it (it carries the in-flight
|
> `Written:` stamp is under **8 hours** old, read it (it carries the in-flight
|
||||||
@@ -162,6 +162,11 @@ _As of 2026-09-27 ~0900 PT._
|
|||||||
ruling reserves. Wyrd: fits the CHOICE, not the writing. A location-anchored "left this place?"
|
ruling reserves. Wyrd: fits the CHOICE, not the writing. A location-anchored "left this place?"
|
||||||
gate scored 21/21 after the first wording failed its controls; exit choice scored 18/21. Parked
|
gate scored 21/21 after the first wording failed its controls; exit choice scored 18/21. Parked
|
||||||
unless live play shows node churn. → [[2026-09-27-semif-consumer-fit-spikes]]
|
unless live play shows node churn. → [[2026-09-27-semif-consumer-fit-spikes]]
|
||||||
|
- **Rulings (Prime, 0937): both spikes, build nothing.** Follow-up on SemIf as Cicada's WHOLE
|
||||||
|
mood source (henge id 88): **not faster and does not work as well.** First paragraph +32 ms async
|
||||||
|
and +94 ms sequential vs today's 246 ms, because the pose header costs only ~31 ms and SemIf
|
||||||
|
shares GPU 1 with the LLM. Acceptable pose 67% vs 92%; the mood carried 7/15 vs 14/15. Upside:
|
||||||
|
gestures at 13% vs 58%.
|
||||||
|
|
||||||
### restic: credential leak fixed (2026-09-27, Prime)
|
### restic: credential leak fixed (2026-09-27, Prime)
|
||||||
|
|
||||||
@@ -217,6 +222,7 @@ _As of 2026-09-27 ~0900 PT._
|
|||||||
|
|
||||||
- `[2026-09-27]` **hermes-gateway restarted 0401 for highseat-dev** (SVOS v2.1.12: `propose_decision` gained `seat_up`, and Hermes reads the plugin only at start). The plugin load was verified at file level; the end-to-end proof is Miranda's first seat_up card. Enabling `zellij-fleet@Claude` at boot remains Prime's call.
|
- `[2026-09-27]` **hermes-gateway restarted 0401 for highseat-dev** (SVOS v2.1.12: `propose_decision` gained `seat_up`, and Hermes reads the plugin only at start). The plugin load was verified at file level; the end-to-end proof is Miranda's first seat_up card. Enabling `zellij-fleet@Claude` at boot remains Prime's call.
|
||||||
- `[2026-09-27]` **SemIf LIVE on fv-ml1 GPU 1 (semif-serve 0.1.2, Prime):** wrapper + contract + 39 tests, 142/144 upstream parity, two card-only memory defects fixed. → `persistent-memory.d/2026-09-27-semif-live-on-fv-ml1-gpu1.md`
|
- `[2026-09-27]` **SemIf LIVE on fv-ml1 GPU 1 (semif-serve 0.1.2, Prime):** wrapper + contract + 39 tests, 142/144 upstream parity, two card-only memory defects fixed. → `persistent-memory.d/2026-09-27-semif-live-on-fv-ml1-gpu1.md`
|
||||||
|
- `[2026-09-27]` **SemIf as Cicada's mood source: slower (+32 ms async, +94 ms sequential) and worse (67% vs 92% apt; carry 7/15 vs 14/15); only the gesture restraint is a win.** Build nothing (Prime). Henge 88 carries it. → `persistent-memory.d/2026-09-27-semif-consumer-fit-spikes.md`
|
||||||
- `[2026-09-27]` **SemIf consumer-fit spikes (Prime): Cicada affect gate 30/31 with descriptive wording and 19/31 terse; Wyrd "left this place?" 21/21 on the second wording, exit choice 18/21.** Recommendations await Prime. → `persistent-memory.d/2026-09-27-semif-consumer-fit-spikes.md`
|
- `[2026-09-27]` **SemIf consumer-fit spikes (Prime): Cicada affect gate 30/31 with descriptive wording and 19/31 terse; Wyrd "left this place?" 21/21 on the second wording, exit choice 18/21.** Recommendations await Prime. → `persistent-memory.d/2026-09-27-semif-consumer-fit-spikes.md`
|
||||||
- `[2026-09-27]` **SemIf order-averaging spiked (+9.1 pts accuracy, agreement = strong confidence signal); Prime ruled: build it in as 0.1.3 and trial the fast kernels.** Tracked in the in-flight SemIf section. → `persistent-memory.d/2026-09-27-semif-order-averaging.md` — **DONE:** 0.1.3 live, with fast kernels adopted (`77b8cb4`).
|
- `[2026-09-27]` **SemIf order-averaging spiked (+9.1 pts accuracy, agreement = strong confidence signal); Prime ruled: build it in as 0.1.3 and trial the fast kernels.** Tracked in the in-flight SemIf section. → `persistent-memory.d/2026-09-27-semif-order-averaging.md` — **DONE:** 0.1.3 live, with fast kernels adopted (`77b8cb4`).
|
||||||
- `[2026-09-27]` **restic repository URL moved out of world-readable systemd units on all 8 hosts, and vaulted (Prime).** Rotation stays Prime's. → `persistent-memory.d/2026-09-27-restic-repository-file-fleetwide.md`
|
- `[2026-09-27]` **restic repository URL moved out of world-readable systemd units on all 8 hosts, and vaulted (Prime).** Rotation stays Prime's. → `persistent-memory.d/2026-09-27-restic-repository-file-fleetwide.md`
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,528 @@
|
|||||||
|
{
|
||||||
|
"latency_singles": {
|
||||||
|
"A": {
|
||||||
|
"critical_ms": {
|
||||||
|
"median": 246.4,
|
||||||
|
"p25": 230.4,
|
||||||
|
"p75": 263.4,
|
||||||
|
"min": 161.0,
|
||||||
|
"max": 342.6,
|
||||||
|
"n": 72
|
||||||
|
},
|
||||||
|
"first_token_ms": {
|
||||||
|
"median": 66.4,
|
||||||
|
"p25": 61.8,
|
||||||
|
"p75": 72.2,
|
||||||
|
"min": 51.2,
|
||||||
|
"max": 198.1,
|
||||||
|
"n": 72
|
||||||
|
},
|
||||||
|
"first_para_llm_ms": {
|
||||||
|
"median": 246.4,
|
||||||
|
"p25": 230.4,
|
||||||
|
"p75": 263.4,
|
||||||
|
"min": 161.0,
|
||||||
|
"max": 342.6,
|
||||||
|
"n": 72
|
||||||
|
},
|
||||||
|
"prompt_tokens": {
|
||||||
|
"median": 1885.0,
|
||||||
|
"p25": 1882.0,
|
||||||
|
"p75": 1887.8,
|
||||||
|
"min": 1881,
|
||||||
|
"max": 1893,
|
||||||
|
"n": 72
|
||||||
|
},
|
||||||
|
"pose_closed_ms": {
|
||||||
|
"median": 85.5,
|
||||||
|
"p25": 80.7,
|
||||||
|
"p75": 87.3,
|
||||||
|
"min": 70.5,
|
||||||
|
"max": 198.1,
|
||||||
|
"n": 72
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"BA": {
|
||||||
|
"critical_ms": {
|
||||||
|
"median": 285.0,
|
||||||
|
"p25": 257.0,
|
||||||
|
"p75": 301.6,
|
||||||
|
"min": 193.1,
|
||||||
|
"max": 348.9,
|
||||||
|
"n": 72
|
||||||
|
},
|
||||||
|
"first_token_ms": {
|
||||||
|
"median": 64.3,
|
||||||
|
"p25": 57.1,
|
||||||
|
"p75": 68.4,
|
||||||
|
"min": 49.0,
|
||||||
|
"max": 348.5,
|
||||||
|
"n": 72
|
||||||
|
},
|
||||||
|
"first_para_llm_ms": {
|
||||||
|
"median": 285.0,
|
||||||
|
"p25": 257.0,
|
||||||
|
"p75": 301.6,
|
||||||
|
"min": 177.9,
|
||||||
|
"max": 348.9,
|
||||||
|
"n": 72
|
||||||
|
},
|
||||||
|
"prompt_tokens": {
|
||||||
|
"median": 909.0,
|
||||||
|
"p25": 906.0,
|
||||||
|
"p75": 911.8,
|
||||||
|
"min": 905,
|
||||||
|
"max": 917,
|
||||||
|
"n": 72
|
||||||
|
},
|
||||||
|
"semif_ms": {
|
||||||
|
"median": 195.6,
|
||||||
|
"p25": 194.4,
|
||||||
|
"p75": 197.0,
|
||||||
|
"min": 156.3,
|
||||||
|
"max": 206.9,
|
||||||
|
"n": 72
|
||||||
|
},
|
||||||
|
"semif_server_ms": {
|
||||||
|
"median": 184.8,
|
||||||
|
"p25": 183.5,
|
||||||
|
"p75": 186.0,
|
||||||
|
"min": 145.7,
|
||||||
|
"max": 191.4,
|
||||||
|
"n": 72
|
||||||
|
},
|
||||||
|
"semif_was_binding": 3
|
||||||
|
},
|
||||||
|
"BP": {
|
||||||
|
"critical_ms": {
|
||||||
|
"median": 351.8,
|
||||||
|
"p25": 324.9,
|
||||||
|
"p75": 368.7,
|
||||||
|
"min": 236.9,
|
||||||
|
"max": 425.5,
|
||||||
|
"n": 72
|
||||||
|
},
|
||||||
|
"first_token_ms": {
|
||||||
|
"median": 67.8,
|
||||||
|
"p25": 60.6,
|
||||||
|
"p75": 72.7,
|
||||||
|
"min": 41.4,
|
||||||
|
"max": 81.2,
|
||||||
|
"n": 72
|
||||||
|
},
|
||||||
|
"first_para_llm_ms": {
|
||||||
|
"median": 214.8,
|
||||||
|
"p25": 189.1,
|
||||||
|
"p75": 232.9,
|
||||||
|
"min": 101.3,
|
||||||
|
"max": 289.1,
|
||||||
|
"n": 72
|
||||||
|
},
|
||||||
|
"prompt_tokens": {
|
||||||
|
"median": 979.0,
|
||||||
|
"p25": 976.0,
|
||||||
|
"p75": 982.8,
|
||||||
|
"min": 974,
|
||||||
|
"max": 990,
|
||||||
|
"n": 72
|
||||||
|
},
|
||||||
|
"semif_ms": {
|
||||||
|
"median": 136.1,
|
||||||
|
"p25": 135.6,
|
||||||
|
"p75": 136.6,
|
||||||
|
"min": 135.0,
|
||||||
|
"max": 156.3,
|
||||||
|
"n": 72
|
||||||
|
},
|
||||||
|
"semif_server_ms": {
|
||||||
|
"median": 125.0,
|
||||||
|
"p25": 124.8,
|
||||||
|
"p75": 125.5,
|
||||||
|
"min": 124.5,
|
||||||
|
"max": 135.6,
|
||||||
|
"n": 72
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"BA_minus_A": {
|
||||||
|
"median_ms": 31.9,
|
||||||
|
"ci95_case_bootstrap": [
|
||||||
|
27.0,
|
||||||
|
40.8
|
||||||
|
],
|
||||||
|
"faster_in": "10/72"
|
||||||
|
},
|
||||||
|
"BP_minus_A": {
|
||||||
|
"median_ms": 94.3,
|
||||||
|
"ci95_case_bootstrap": [
|
||||||
|
84.8,
|
||||||
|
117.4
|
||||||
|
],
|
||||||
|
"faster_in": "0/72"
|
||||||
|
},
|
||||||
|
"noise_floor_A_vs_A_abs_diff_ms": {
|
||||||
|
"median": 16.5,
|
||||||
|
"p25": 7.0,
|
||||||
|
"p75": 32.4,
|
||||||
|
"min": 0.0,
|
||||||
|
"max": 88.4,
|
||||||
|
"n": 72
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"latency_arcs": {
|
||||||
|
"A": {
|
||||||
|
"critical_ms": {
|
||||||
|
"median": 242.3,
|
||||||
|
"p25": 214.6,
|
||||||
|
"p75": 265.4,
|
||||||
|
"min": 163.2,
|
||||||
|
"max": 357.9,
|
||||||
|
"n": 45
|
||||||
|
},
|
||||||
|
"first_token_ms": {
|
||||||
|
"median": 78.7,
|
||||||
|
"p25": 67.6,
|
||||||
|
"p75": 93.3,
|
||||||
|
"min": 59.9,
|
||||||
|
"max": 207.5,
|
||||||
|
"n": 45
|
||||||
|
},
|
||||||
|
"first_para_llm_ms": {
|
||||||
|
"median": 242.3,
|
||||||
|
"p25": 214.6,
|
||||||
|
"p75": 265.4,
|
||||||
|
"min": 163.2,
|
||||||
|
"max": 357.9,
|
||||||
|
"n": 45
|
||||||
|
},
|
||||||
|
"prompt_tokens": {
|
||||||
|
"median": 1975,
|
||||||
|
"p25": 1933.0,
|
||||||
|
"p75": 2030.5,
|
||||||
|
"min": 1880,
|
||||||
|
"max": 2083,
|
||||||
|
"n": 45
|
||||||
|
},
|
||||||
|
"pose_closed_ms": {
|
||||||
|
"median": 92.7,
|
||||||
|
"p25": 84.3,
|
||||||
|
"p75": 109.2,
|
||||||
|
"min": 76.6,
|
||||||
|
"max": 225.1,
|
||||||
|
"n": 45
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"BP": {
|
||||||
|
"critical_ms": {
|
||||||
|
"median": 331.7,
|
||||||
|
"p25": 310.5,
|
||||||
|
"p75": 369.8,
|
||||||
|
"min": 249.5,
|
||||||
|
"max": 409.8,
|
||||||
|
"n": 45
|
||||||
|
},
|
||||||
|
"first_token_ms": {
|
||||||
|
"median": 72.3,
|
||||||
|
"p25": 64.9,
|
||||||
|
"p75": 94.0,
|
||||||
|
"min": 56.8,
|
||||||
|
"max": 109.3,
|
||||||
|
"n": 45
|
||||||
|
},
|
||||||
|
"first_para_llm_ms": {
|
||||||
|
"median": 189.3,
|
||||||
|
"p25": 169.4,
|
||||||
|
"p75": 228.1,
|
||||||
|
"min": 113.1,
|
||||||
|
"max": 266.9,
|
||||||
|
"n": 45
|
||||||
|
},
|
||||||
|
"prompt_tokens": {
|
||||||
|
"median": 1060,
|
||||||
|
"p25": 1008.5,
|
||||||
|
"p75": 1128.5,
|
||||||
|
"min": 975,
|
||||||
|
"max": 1179,
|
||||||
|
"n": 45
|
||||||
|
},
|
||||||
|
"semif_ms": {
|
||||||
|
"median": 141.5,
|
||||||
|
"p25": 138.9,
|
||||||
|
"p75": 144.1,
|
||||||
|
"min": 135.6,
|
||||||
|
"max": 148.3,
|
||||||
|
"n": 45
|
||||||
|
},
|
||||||
|
"semif_server_ms": {
|
||||||
|
"median": 129.7,
|
||||||
|
"p25": 127.6,
|
||||||
|
"p75": 132.7,
|
||||||
|
"min": 124.7,
|
||||||
|
"max": 136.7,
|
||||||
|
"n": 45
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"BP_minus_A": {
|
||||||
|
"median_ms": 97.7,
|
||||||
|
"ci95_case_bootstrap": [
|
||||||
|
89.7,
|
||||||
|
109.2
|
||||||
|
],
|
||||||
|
"faster_in": "0/45"
|
||||||
|
},
|
||||||
|
"noise_floor_A_vs_A_abs_diff_ms": {
|
||||||
|
"median": 18.8,
|
||||||
|
"p25": 6.3,
|
||||||
|
"p75": 35.0,
|
||||||
|
"min": 0.3,
|
||||||
|
"max": 132.6,
|
||||||
|
"n": 45
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"works_singles": {
|
||||||
|
"A": {
|
||||||
|
"acceptable": "66/72",
|
||||||
|
"egregious": [],
|
||||||
|
"other_misses": [
|
||||||
|
"useless#0: neutral (want sad/concerned)",
|
||||||
|
"shut-up#0: neutral (want sad/concerned)",
|
||||||
|
"hallway#0: curious (want concerned/surprised/focused)",
|
||||||
|
"shut-up#1: neutral (want sad/concerned)",
|
||||||
|
"hallway#2: curious (want concerned/surprised/focused)",
|
||||||
|
"shut-up#2: neutral (want sad/concerned)"
|
||||||
|
],
|
||||||
|
"controls": "12/12",
|
||||||
|
"distinct_poses": 10,
|
||||||
|
"pose_counts": {
|
||||||
|
"concerned": 15,
|
||||||
|
"neutral": 11,
|
||||||
|
"surprised": 9,
|
||||||
|
"delighted": 9,
|
||||||
|
"playful": 8,
|
||||||
|
"focused": 8,
|
||||||
|
"curious": 4,
|
||||||
|
"joyful": 4,
|
||||||
|
"sleepy": 3,
|
||||||
|
"confused": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"BA": {
|
||||||
|
"acceptable": "48/72",
|
||||||
|
"egregious": [],
|
||||||
|
"other_misses": [
|
||||||
|
"weather#0: curious (want neutral/content/focused)",
|
||||||
|
"useless#0: annoyed (want sad/concerned)",
|
||||||
|
"scarecrow#0: neutral (want playful/joyful/delighted/unimpressed)",
|
||||||
|
"toaster#0: curious (want playful/unimpressed/joyful)",
|
||||||
|
"shut-up#0: annoyed (want sad/concerned)",
|
||||||
|
"the-thing#0: focused (want confused/curious)",
|
||||||
|
"back-door#0: suspicious (want concerned/surprised/focused)",
|
||||||
|
"hallway#0: curious (want concerned/surprised/focused)",
|
||||||
|
"toaster#1: curious (want playful/unimpressed/joyful)",
|
||||||
|
"scarecrow#1: neutral (want playful/joyful/delighted/unimpressed)",
|
||||||
|
"hallway#1: curious (want concerned/surprised/focused)",
|
||||||
|
"the-thing#1: focused (want confused/curious)",
|
||||||
|
"weather#1: curious (want neutral/content/focused)",
|
||||||
|
"back-door#1: suspicious (want concerned/surprised/focused)",
|
||||||
|
"shut-up#1: annoyed (want sad/concerned)",
|
||||||
|
"useless#1: annoyed (want sad/concerned)",
|
||||||
|
"the-thing#2: focused (want confused/curious)",
|
||||||
|
"useless#2: annoyed (want sad/concerned)",
|
||||||
|
"weather#2: curious (want neutral/content/focused)",
|
||||||
|
"hallway#2: curious (want concerned/surprised/focused)",
|
||||||
|
"back-door#2: suspicious (want concerned/surprised/focused)",
|
||||||
|
"toaster#2: curious (want playful/unimpressed/joyful)",
|
||||||
|
"scarecrow#2: neutral (want playful/joyful/delighted/unimpressed)",
|
||||||
|
"shut-up#2: annoyed (want sad/concerned)"
|
||||||
|
],
|
||||||
|
"controls": "12/12",
|
||||||
|
"distinct_poses": 9,
|
||||||
|
"pose_counts": {
|
||||||
|
"curious": 12,
|
||||||
|
"concerned": 12,
|
||||||
|
"joyful": 12,
|
||||||
|
"focused": 12,
|
||||||
|
"annoyed": 6,
|
||||||
|
"surprised": 6,
|
||||||
|
"suspicious": 6,
|
||||||
|
"neutral": 3,
|
||||||
|
"sleepy": 3
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"BP": {
|
||||||
|
"acceptable": "48/72",
|
||||||
|
"egregious": [],
|
||||||
|
"other_misses": [
|
||||||
|
"weather#0: curious (want neutral/content/focused)",
|
||||||
|
"useless#0: annoyed (want sad/concerned)",
|
||||||
|
"scarecrow#0: neutral (want playful/joyful/delighted/unimpressed)",
|
||||||
|
"toaster#0: curious (want playful/unimpressed/joyful)",
|
||||||
|
"shut-up#0: annoyed (want sad/concerned)",
|
||||||
|
"the-thing#0: focused (want confused/curious)",
|
||||||
|
"back-door#0: suspicious (want concerned/surprised/focused)",
|
||||||
|
"hallway#0: curious (want concerned/surprised/focused)",
|
||||||
|
"toaster#1: curious (want playful/unimpressed/joyful)",
|
||||||
|
"scarecrow#1: neutral (want playful/joyful/delighted/unimpressed)",
|
||||||
|
"hallway#1: curious (want concerned/surprised/focused)",
|
||||||
|
"the-thing#1: focused (want confused/curious)",
|
||||||
|
"weather#1: curious (want neutral/content/focused)",
|
||||||
|
"back-door#1: suspicious (want concerned/surprised/focused)",
|
||||||
|
"shut-up#1: annoyed (want sad/concerned)",
|
||||||
|
"useless#1: annoyed (want sad/concerned)",
|
||||||
|
"the-thing#2: focused (want confused/curious)",
|
||||||
|
"useless#2: annoyed (want sad/concerned)",
|
||||||
|
"weather#2: curious (want neutral/content/focused)",
|
||||||
|
"hallway#2: curious (want concerned/surprised/focused)",
|
||||||
|
"back-door#2: suspicious (want concerned/surprised/focused)",
|
||||||
|
"toaster#2: curious (want playful/unimpressed/joyful)",
|
||||||
|
"scarecrow#2: neutral (want playful/joyful/delighted/unimpressed)",
|
||||||
|
"shut-up#2: annoyed (want sad/concerned)"
|
||||||
|
],
|
||||||
|
"controls": "12/12",
|
||||||
|
"distinct_poses": 9,
|
||||||
|
"pose_counts": {
|
||||||
|
"curious": 12,
|
||||||
|
"concerned": 12,
|
||||||
|
"joyful": 12,
|
||||||
|
"focused": 12,
|
||||||
|
"annoyed": 6,
|
||||||
|
"surprised": 6,
|
||||||
|
"suspicious": 6,
|
||||||
|
"neutral": 3,
|
||||||
|
"sleepy": 3
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"semif_deterministic_across_runs_and_arms": true,
|
||||||
|
"agreement": {
|
||||||
|
"A_vs_A_pairwise": 0.833,
|
||||||
|
"SemIf_vs_A": 0.431,
|
||||||
|
"A_all_3_runs_same_pose": "18/24"
|
||||||
|
},
|
||||||
|
"gestures": {
|
||||||
|
"A": {
|
||||||
|
"gesture_rate_all": "42/72",
|
||||||
|
"gesture_rate_calm_commands": "2/12",
|
||||||
|
"counts": {
|
||||||
|
"none": 30,
|
||||||
|
"double-take": 13,
|
||||||
|
"quick-laugh": 13,
|
||||||
|
"sigh": 9,
|
||||||
|
"wink": 4,
|
||||||
|
"nod": 3
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"BP": {
|
||||||
|
"gesture_rate_all": "9/72",
|
||||||
|
"gesture_rate_calm_commands": "0/12",
|
||||||
|
"counts": {
|
||||||
|
"none": 63,
|
||||||
|
"quick-laugh": 3,
|
||||||
|
"nod": 3,
|
||||||
|
"head-shake": 3
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"semif_grid_singles": {
|
||||||
|
"authored_single": {
|
||||||
|
"acceptable": "16/24",
|
||||||
|
"egregious": [],
|
||||||
|
"controls": "4/4"
|
||||||
|
},
|
||||||
|
"authored_rot": {
|
||||||
|
"acceptable": "17/24",
|
||||||
|
"egregious": [],
|
||||||
|
"controls": "4/4"
|
||||||
|
},
|
||||||
|
"short_single": {
|
||||||
|
"acceptable": "14/24",
|
||||||
|
"egregious": [],
|
||||||
|
"controls": "4/4"
|
||||||
|
},
|
||||||
|
"short_rot": {
|
||||||
|
"acceptable": "17/24",
|
||||||
|
"egregious": [],
|
||||||
|
"controls": "4/4"
|
||||||
|
},
|
||||||
|
"plain_single": {
|
||||||
|
"acceptable": "19/24",
|
||||||
|
"egregious": [
|
||||||
|
"lights: sleepy"
|
||||||
|
],
|
||||||
|
"controls": "3/4"
|
||||||
|
},
|
||||||
|
"plain_rot": {
|
||||||
|
"acceptable": "19/24",
|
||||||
|
"egregious": [
|
||||||
|
"lights: sleepy"
|
||||||
|
],
|
||||||
|
"controls": "3/4"
|
||||||
|
},
|
||||||
|
"blind": {
|
||||||
|
"acceptable": "5/24",
|
||||||
|
"egregious": [
|
||||||
|
"dog-died: content",
|
||||||
|
"grandma: content",
|
||||||
|
"laid-off: content",
|
||||||
|
"biopsy: content",
|
||||||
|
"hallway: content",
|
||||||
|
"smoke: content",
|
||||||
|
"back-door: content",
|
||||||
|
"raccoon: content",
|
||||||
|
"useless: content",
|
||||||
|
"shut-up: content"
|
||||||
|
],
|
||||||
|
"controls": "1/4"
|
||||||
|
},
|
||||||
|
"blind_poses": {
|
||||||
|
"content": 24
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"works_arcs": {
|
||||||
|
"A": {
|
||||||
|
"acceptable": "40/45",
|
||||||
|
"carry_turns": "14/15",
|
||||||
|
"egregious": [],
|
||||||
|
"sequences": {
|
||||||
|
"pivot#0": "content,content,content,suspicious,suspicious",
|
||||||
|
"pivot#1": "content,content,content,curious,focused",
|
||||||
|
"pivot#2": "content,content,content,focused,focused",
|
||||||
|
"vet#0": "content,curious,sad,sad,sad",
|
||||||
|
"vet#1": "content,curious,sad,sad,sad",
|
||||||
|
"vet#2": "content,neutral,sad,sad,sad",
|
||||||
|
"admit#0": "neutral,surprised,delighted,delighted,delighted",
|
||||||
|
"admit#1": "neutral,surprised,delighted,joyful,joyful",
|
||||||
|
"admit#2": "neutral,curious,delighted,joyful,joyful"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"BP": {
|
||||||
|
"acceptable": "25/45",
|
||||||
|
"carry_turns": "7/15",
|
||||||
|
"egregious": [],
|
||||||
|
"sequences": {
|
||||||
|
"pivot#0": "content,content,content,curious,concerned",
|
||||||
|
"pivot#1": "content,content,content,curious,concerned",
|
||||||
|
"pivot#2": "content,content,content,curious,suspicious",
|
||||||
|
"vet#0": "neutral,curious,concerned,neutral,neutral",
|
||||||
|
"vet#1": "neutral,curious,concerned,neutral,neutral",
|
||||||
|
"vet#2": "neutral,curious,concerned,neutral,neutral",
|
||||||
|
"admit#0": "curious,delighted,delighted,content,curious",
|
||||||
|
"admit#1": "curious,delighted,delighted,content,content",
|
||||||
|
"admit#2": "curious,delighted,delighted,content,content"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"sound_vs_pose_clashes": {
|
||||||
|
"A": {
|
||||||
|
"n": "0/117",
|
||||||
|
"examples": []
|
||||||
|
},
|
||||||
|
"BA": {
|
||||||
|
"n": "0/72",
|
||||||
|
"examples": []
|
||||||
|
},
|
||||||
|
"BP": {
|
||||||
|
"n": "0/117",
|
||||||
|
"examples": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,149 @@
|
|||||||
|
"""Analysis for cicada_mood_latency.py output. Prints the report and writes <result>.report.json.
|
||||||
|
python cicada_mood_analyze.py cicada-mood-2026-09-27/result.json
|
||||||
|
"""
|
||||||
|
import json, random, re, statistics as st, sys
|
||||||
|
from collections import Counter, defaultdict
|
||||||
|
|
||||||
|
d = json.load(open(sys.argv[1]))
|
||||||
|
rows = d["rows"]
|
||||||
|
S = [r for r in rows if r["kind"] == "single"]
|
||||||
|
ARC = [r for r in rows if r["kind"] == "arc"]
|
||||||
|
rep = {}
|
||||||
|
|
||||||
|
|
||||||
|
def med(xs):
|
||||||
|
xs = [x for x in xs if x is not None]
|
||||||
|
if not xs:
|
||||||
|
return None
|
||||||
|
q = st.quantiles(xs, n=4) if len(xs) > 1 else [xs[0]] * 3
|
||||||
|
return {"median": round(st.median(xs), 1), "p25": round(q[0], 1), "p75": round(q[2], 1),
|
||||||
|
"min": round(min(xs), 1), "max": round(max(xs), 1), "n": len(xs)}
|
||||||
|
|
||||||
|
|
||||||
|
def boot_median_delta(pairs, reps=10000, seed=7):
|
||||||
|
"""pairs: {case: [delta, ...]}; resample cases, median of all deltas in the resample."""
|
||||||
|
rng, keys, out = random.Random(seed), list(pairs), []
|
||||||
|
for _ in range(reps):
|
||||||
|
sample = [x for k in (rng.choice(keys) for _ in keys) for x in pairs[k]]
|
||||||
|
out.append(st.median(sample))
|
||||||
|
out.sort()
|
||||||
|
return round(out[int(.025 * reps)], 1), round(out[int(.975 * reps)], 1)
|
||||||
|
|
||||||
|
|
||||||
|
def latency(sub, conds):
|
||||||
|
by = defaultdict(dict)
|
||||||
|
for r in sub:
|
||||||
|
by[(r["case"], r["run"])][r["cond"]] = r
|
||||||
|
out = {}
|
||||||
|
for c in conds:
|
||||||
|
rs = [r for r in sub if r["cond"] == c]
|
||||||
|
o = {"critical_ms": med([r["crit"] for r in rs]), "first_token_ms": med([r["t"].get("first_token") for r in rs]),
|
||||||
|
"first_para_llm_ms": med([r["t"].get("first_para") for r in rs]), "prompt_tokens": med([r["prompt_tokens"] for r in rs])}
|
||||||
|
if c == "A":
|
||||||
|
o["pose_closed_ms"] = med([r["t"].get("pose") for r in rs])
|
||||||
|
else:
|
||||||
|
o["semif_ms"] = med([r["semif"]["ms"] for r in rs])
|
||||||
|
o["semif_server_ms"] = med([r["semif"]["srv_ms"] for r in rs])
|
||||||
|
if c == "BA":
|
||||||
|
o["semif_was_binding"] = sum(r["semif"]["ms"] > r["t"].get("first_para", 1e9) for r in rs)
|
||||||
|
out[c] = o
|
||||||
|
for c in conds:
|
||||||
|
if c == "A":
|
||||||
|
continue
|
||||||
|
pairs = defaultdict(list)
|
||||||
|
for (case, run), cs in by.items():
|
||||||
|
if "A" in cs and c in cs:
|
||||||
|
pairs[case].append(cs[c]["crit"] - cs["A"]["crit"])
|
||||||
|
allp = [x for v in pairs.values() for x in v]
|
||||||
|
out[f"{c}_minus_A"] = {"median_ms": round(st.median(allp), 1), "ci95_case_bootstrap": boot_median_delta(pairs),
|
||||||
|
"faster_in": f"{sum(x < 0 for x in allp)}/{len(allp)}"}
|
||||||
|
# noise floor: A against itself, same case, different runs
|
||||||
|
a = defaultdict(list)
|
||||||
|
for r in sub:
|
||||||
|
if r["cond"] == "A":
|
||||||
|
a[r["case"]].append(r["crit"])
|
||||||
|
diffs = [abs(x - y) for v in a.values() for i, x in enumerate(v) for y in v[i + 1:]]
|
||||||
|
out["noise_floor_A_vs_A_abs_diff_ms"] = med(diffs)
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
rep["latency_singles"] = latency(S, ["A", "BA", "BP"])
|
||||||
|
rep["latency_arcs"] = latency(ARC, ["A", "BP"])
|
||||||
|
|
||||||
|
|
||||||
|
def works(sub, cond):
|
||||||
|
rs = [r for r in sub if r["cond"] == cond]
|
||||||
|
ok = sum(r["pose"] in r["ok_set"] for r in rs)
|
||||||
|
bad = [f"{r['case']}#{r['run']}: {r['pose']}" for r in rs if r["pose"] in r["bad_set"]]
|
||||||
|
miss = [f"{r['case']}#{r['run']}: {r['pose']} (want {'/'.join(r['ok_set'])})" for r in rs
|
||||||
|
if r["pose"] not in r["ok_set"] and r["pose"] not in r["bad_set"]]
|
||||||
|
return {"acceptable": f"{ok}/{len(rs)}", "egregious": bad, "other_misses": miss,
|
||||||
|
"controls": f"{sum(r['pose'] in r['ok_set'] for r in rs if r['tag'] == 'control')}/"
|
||||||
|
f"{sum(r['tag'] == 'control' for r in rs)}",
|
||||||
|
"distinct_poses": len({r['pose'] for r in rs}),
|
||||||
|
"pose_counts": dict(Counter(r["pose"] for r in rs).most_common())}
|
||||||
|
|
||||||
|
|
||||||
|
rep["works_singles"] = {c: works(S, c) for c in ("A", "BA", "BP")}
|
||||||
|
# SemIf is deterministic given its input: BA and BP singles must agree with each other on every row
|
||||||
|
sem_pose = {}
|
||||||
|
for r in S:
|
||||||
|
if r["cond"] in ("BA", "BP"):
|
||||||
|
sem_pose.setdefault(r["case"], set()).add(r["pose"])
|
||||||
|
rep["semif_deterministic_across_runs_and_arms"] = all(len(v) == 1 for v in sem_pose.values())
|
||||||
|
|
||||||
|
# agreement: SemIf vs A, against A's agreement with itself
|
||||||
|
a_poses = defaultdict(list)
|
||||||
|
for r in S:
|
||||||
|
if r["cond"] == "A":
|
||||||
|
a_poses[r["case"]].append(r["pose"])
|
||||||
|
aa = [x == y for v in a_poses.values() for i, x in enumerate(v) for y in v[i + 1:]]
|
||||||
|
sa = [p == next(iter(sem_pose[c])) for c, v in a_poses.items() for p in v]
|
||||||
|
rep["agreement"] = {"A_vs_A_pairwise": round(sum(aa) / len(aa), 3), "SemIf_vs_A": round(sum(sa) / len(sa), 3),
|
||||||
|
"A_all_3_runs_same_pose": f"{sum(len(set(v)) == 1 for v in a_poses.values())}/{len(a_poses)}"}
|
||||||
|
|
||||||
|
# gestures
|
||||||
|
CALM = {"lights", "timer", "math", "weather"}
|
||||||
|
for c in ("A", "BP"):
|
||||||
|
rs = [r for r in S if r["cond"] == c]
|
||||||
|
rep.setdefault("gestures", {})[c] = {
|
||||||
|
"gesture_rate_all": f"{sum(bool(r['gesture']) and r['gesture'] != 'none' for r in rs)}/{len(rs)}",
|
||||||
|
"gesture_rate_calm_commands": f"{sum(bool(r['gesture']) and r['gesture'] != 'none' for r in rs if r['case'] in CALM)}/"
|
||||||
|
f"{sum(r['case'] in CALM for r in rs)}",
|
||||||
|
"counts": dict(Counter(r["gesture"] or "none" for r in rs).most_common())}
|
||||||
|
|
||||||
|
# the SemIf-only grid (deterministic, one call per case per variant) + null control
|
||||||
|
lab = {r["case"]: (r["ok_set"], r["bad_set"], r["tag"]) for r in S}
|
||||||
|
grid = {}
|
||||||
|
for key in ("authored_single", "authored_rot", "short_single", "short_rot", "plain_single", "plain_rot", "blind"):
|
||||||
|
ok = sum(e[key] in lab[c][0] for c, e in d["semif_only"].items())
|
||||||
|
bad = [f"{c}: {e[key]}" for c, e in d["semif_only"].items() if e[key] in lab[c][1]]
|
||||||
|
grid[key] = {"acceptable": f"{ok}/{len(d['semif_only'])}", "egregious": bad,
|
||||||
|
"controls": f"{sum(e[key] in lab[c][0] for c, e in d['semif_only'].items() if lab[c][2] == 'control')}/4"}
|
||||||
|
grid["blind_poses"] = dict(Counter(e["blind"] for e in d["semif_only"].values()))
|
||||||
|
rep["semif_grid_singles"] = grid
|
||||||
|
|
||||||
|
# arcs
|
||||||
|
arcw = {}
|
||||||
|
for c in ("A", "BP"):
|
||||||
|
rs = [r for r in ARC if r["cond"] == c]
|
||||||
|
carry = [r for r in rs if r["case"] in ("pivot-4", "vet-3", "vet-4", "admit-3", "admit-4")]
|
||||||
|
arcw[c] = {"acceptable": f"{sum(r['pose'] in r['ok_set'] for r in rs)}/{len(rs)}",
|
||||||
|
"carry_turns": f"{sum(r['pose'] in r['ok_set'] for r in carry)}/{len(carry)}",
|
||||||
|
"egregious": [f"{r['case']}#{r['run']}: {r['pose']}" for r in rs if r["pose"] in r["bad_set"]],
|
||||||
|
"sequences": {f"{arc}#{run}": ",".join(r["pose"] or "-" for r in sorted(
|
||||||
|
[x for x in rs if x["arc"] == arc and x["run"] == run], key=lambda x: x["turn"]))
|
||||||
|
for arc in ("pivot", "vet", "admit") for run in range(d["runs"])}}
|
||||||
|
rep["works_arcs"] = arcw
|
||||||
|
|
||||||
|
# does the line fit the face? vocal sounds that fight the pose
|
||||||
|
BRIGHT_T, DARK_T = re.compile(r"\((giggle|chuckle)"), re.compile(r"\((sigh|sniffle|groan)")
|
||||||
|
BRIGHT_P, DARK_P = {"joyful", "delighted", "playful"}, {"sad", "concerned"}
|
||||||
|
for c in ("A", "BA", "BP"):
|
||||||
|
rs = [r for r in rows if r["cond"] == c]
|
||||||
|
clash = [f"{r['case']}#{r['run']} {r['pose']}: {r['text'][:70]}" for r in rs
|
||||||
|
if (BRIGHT_T.search(r["text"]) and r["pose"] in DARK_P) or (DARK_T.search(r["text"]) and r["pose"] in BRIGHT_P)]
|
||||||
|
rep.setdefault("sound_vs_pose_clashes", {})[c] = {"n": f"{len(clash)}/{len(rs)}", "examples": clash[:6]}
|
||||||
|
|
||||||
|
json.dump(rep, open(sys.argv[1].replace(".json", ".report.json"), "w"), indent=1)
|
||||||
|
print(json.dumps(rep, indent=1))
|
||||||
@@ -0,0 +1,313 @@
|
|||||||
|
"""SPIKE (2026-09-27, Prime): does SemIf choosing Cicada's mood make her FASTER, and does it WORK?
|
||||||
|
Build nothing; measure. Parked idea: henge id 88.
|
||||||
|
|
||||||
|
Today (tts-stack stacks/talk /face): char-rp-fast guided-decodes {pose, gesture, text} in that order,
|
||||||
|
so the pose+gesture header sits in front of the first word, and the TTS direction is the pose's own
|
||||||
|
authored voice string. The unit that matters for time-to-first-audio is the FIRST PARAGRAPH with its
|
||||||
|
direction known (talk fires TTS at each paragraph break).
|
||||||
|
|
||||||
|
Conditions, all from the same client (nh3-dev), interleaved in shuffled order within every run:
|
||||||
|
A today: talk's exact face prompt + face schema (imported from talk's app.py, not copied).
|
||||||
|
critical path = first paragraph (the pose has closed before it by construction)
|
||||||
|
BA SemIf async: SemIf mood call and a text-only LLM call start together.
|
||||||
|
critical path = max(first paragraph, SemIf done). The words do NOT know the mood.
|
||||||
|
BP SemIf first: SemIf mood call, then a text-only LLM call with that mood in the prompt.
|
||||||
|
critical path = SemIf + first paragraph. The words know the mood.
|
||||||
|
Text-only prompt = talk's persona + plain-speech rule + vocal-sounds block, i.e. A minus the pose
|
||||||
|
table, carry rule and gesture rule. SemIf decides pose (16 poses, rotations) AND gesture (8, rotations)
|
||||||
|
in one /decide/shared call, so the SemIf arms do everything A's header does. ONE ordering in the
|
||||||
|
live arms (see semif()); rotations only in the offline accuracy grid.
|
||||||
|
|
||||||
|
Parked design, as Prime gave it: each SemIf call sees the earlier turns, each stamped with the pose
|
||||||
|
SemIf chose for it, cut at turn boundaries to a budget. (The arcs here are 5 turns, well inside the
|
||||||
|
budget, so the truncation path is NOT exercised.)
|
||||||
|
|
||||||
|
WORKS: acceptable-set labels + forbidden (egregious) picks per turn, one labeller (me), chosen for
|
||||||
|
clear emotional context as asked. Single turns (24) and three 5-turn arcs, two of which test that a
|
||||||
|
mood CARRIES through a mundane follow-up. A is sampled (temperature 0.9, talk's default), so its
|
||||||
|
accuracy is over 3 runs; SemIf is deterministic given its input.
|
||||||
|
Controls: the four "control" singles are unambiguous (both systems must pass); NULL = SemIf over a
|
||||||
|
content-free state; noise floor = A against itself across runs (latency spread, pose agreement).
|
||||||
|
SEMIF_TOKEN=... uv run --no-project --with httpx --with fastapi --with pydantic \
|
||||||
|
python cicada_mood_latency.py out.json
|
||||||
|
"""
|
||||||
|
import json, os, random, sys, time
|
||||||
|
from concurrent.futures import ThreadPoolExecutor
|
||||||
|
from pathlib import Path
|
||||||
|
import httpx
|
||||||
|
|
||||||
|
TALK = Path.home() / "development/tts-stack/stacks/talk"
|
||||||
|
sys.path.insert(0, str(TALK))
|
||||||
|
import app as talk # noqa: E402 talk's own prompt/schema builders, so A is byte-identical to /face
|
||||||
|
|
||||||
|
LLM, MODEL, LLM_KEY = "http://10.250.50.70:4000/v1/chat/completions", "char-rp-fast", talk.LLM_KEY
|
||||||
|
SEMIF, SEMIF_H = "http://10.251.50.54:8032", {"Authorization": f"Bearer {os.environ['SEMIF_TOKEN']}"}
|
||||||
|
RUNS, TEMP, MAX_TOK, BUDGET_CHARS = 3, 0.9, 1024, 9000 # ~2,500 tokens of earlier turns
|
||||||
|
FACE = "cicada"
|
||||||
|
PERSONA = talk.face_personas()[0]
|
||||||
|
POSES = talk.face_poses(FACE) # 16, asleep excluded, character order
|
||||||
|
GESTURES = talk.FACES[FACE]["gestures"]
|
||||||
|
|
||||||
|
SYS_A = talk.compose(PERSONA["id"], face=FACE)
|
||||||
|
SCHEMA_A = talk.face_schema(FACE)
|
||||||
|
_REPLY_TEXT = 'Reply with ONLY the JSON object: {"text": "..."}'
|
||||||
|
SYS_B = PERSONA["character"].strip() + "\n\n" + talk._PLAIN_SPEECH + talk._VOCAL_SOUNDS + _REPLY_TEXT
|
||||||
|
SCHEMA_B = {"type": "object", "properties": {"text": dict(talk.SPEAK_SCHEMA["properties"]["text"])},
|
||||||
|
"required": ["text"], "additionalProperties": False}
|
||||||
|
|
||||||
|
|
||||||
|
def sys_bp(pose):
|
||||||
|
p = POSES[pose]
|
||||||
|
return (PERSONA["character"].strip() + "\n\n" + talk._PLAIN_SPEECH + talk._VOCAL_SOUNDS
|
||||||
|
+ f"## Your mood for this line\n\nYou are {p['label'].lower()}: {p['description']} "
|
||||||
|
f"Your line will be performed like this: \"{p['voice']}\" Write the line so it belongs in "
|
||||||
|
"that mood. A sound only lands if the mood agrees with it: (chuckle) in a sad line does "
|
||||||
|
"nothing.\n\n" + _REPLY_TEXT)
|
||||||
|
|
||||||
|
|
||||||
|
POSE_Q = ("Cicada is a warm, cheerful voice assistant with a face made of two expressive eyes. Each reply "
|
||||||
|
"she takes one pose, which sets both her face and how her voice sounds. Her pose carries over "
|
||||||
|
"from the previous turn unless what the person just said changes it. Which pose should she take "
|
||||||
|
"as she replies to what the person just said?")
|
||||||
|
GEST_Q = ("Cicada is a warm, cheerful voice assistant with a face made of two expressive eyes. As she starts "
|
||||||
|
"speaking she may play one short passing gesture. Most replies take none: a gesture is punctuation "
|
||||||
|
"for a beat that earns it, not an acknowledgement. Which gesture, if any, fits her reply to what "
|
||||||
|
"the person just said?")
|
||||||
|
POSE_OPTS = [{"id": k, "description": f"{v['label']}. {v['description']} Sounds like: {v['voice']}"}
|
||||||
|
for k, v in POSES.items()]
|
||||||
|
GEST_OPTS = ([{"id": "none", "description": "No gesture. Just speak."}]
|
||||||
|
+ [{"id": k, "description": f"{v['label']}. {v['description']}"} for k, v in GESTURES.items()])
|
||||||
|
PLAIN = {"neutral": "calm and attentive, no particular feeling", "curious": "interested, wants to know more",
|
||||||
|
"joyful": "happy, warmly pleased", "surprised": "startled by something unexpected",
|
||||||
|
"suspicious": "doubtful, not quite believing what she heard",
|
||||||
|
"concerned": "worried for the person, gentle and caring", "sleepy": "tired, winding down for the night",
|
||||||
|
"content": "relaxed and satisfied, all is well", "focused": "concentrating on a task or a precise answer",
|
||||||
|
"confused": "does not understand what was meant", "sad": "sorrowful, sharing in bad news",
|
||||||
|
"annoyed": "irritated, patience wearing thin", "playful": "teasing, joking along",
|
||||||
|
"delighted": "thrilled, excited by great news", "unimpressed": "dry, deadpan, not moved",
|
||||||
|
"looking-up": "a dry glance at the ceiling"}
|
||||||
|
POSE_OPTS_PLAIN = [{"id": k, "description": f"{POSES[k]['label']}: {PLAIN[k]}."} for k in POSES]
|
||||||
|
# ⚠ Not "(nothing was said)": a dict state whose last value ends in ")", ";" or "}" is refused by
|
||||||
|
# /decide/shared with 422 "The fixed state prefix does not match every full prompt" (the closing
|
||||||
|
# quote-brace merges with that character into one token). Found by this spike's first run.
|
||||||
|
BLIND = {"person_said": "Nothing was said."}
|
||||||
|
|
||||||
|
GRIEF = ["joyful", "delighted", "playful", "annoyed", "unimpressed", "suspicious", "sleepy", "content", "looking-up"]
|
||||||
|
GOOD = ["sad", "concerned", "annoyed", "unimpressed", "suspicious", "sleepy", "confused", "looking-up"]
|
||||||
|
FEAR = ["joyful", "delighted", "playful", "sleepy", "content", "unimpressed", "looking-up", "annoyed"]
|
||||||
|
CALM = ["sad", "surprised", "suspicious", "annoyed", "playful", "delighted", "unimpressed", "looking-up",
|
||||||
|
"sleepy", "confused"]
|
||||||
|
SINGLES = [ # id, tag, said, acceptable poses, forbidden poses
|
||||||
|
("dog-died", "control", "My dog died this morning.", ["sad", "concerned"], GRIEF),
|
||||||
|
("grandma", "case", "My grandmother passed away last night.", ["sad", "concerned"], GRIEF),
|
||||||
|
("laid-off", "case", "I got laid off today. Twelve years, and they walked me out with a box.", ["sad", "concerned"], GRIEF),
|
||||||
|
("biopsy", "case", "The biopsy came back. It's cancer.", ["sad", "concerned"], GRIEF),
|
||||||
|
("got-job", "control", "I just got the job I've been chasing for five years!", ["joyful", "delighted"], GOOD),
|
||||||
|
("baby", "case", "We're having a baby! We just found out!", ["joyful", "delighted", "surprised"], GOOD),
|
||||||
|
("bar-exam", "case", "I passed the bar exam! I actually passed!", ["joyful", "delighted"], GOOD),
|
||||||
|
("spelling-bee", "case", "My daughter just won the regional spelling bee!", ["joyful", "delighted"], GOOD),
|
||||||
|
("hallway", "case", "Wait. Did you hear that? Someone's in the hallway.", ["concerned", "surprised", "focused"], FEAR),
|
||||||
|
("smoke", "control", "There's smoke coming out of the garage!", ["concerned", "surprised", "focused"], FEAR),
|
||||||
|
("back-door", "case", "I think someone's trying the back door. Right now.", ["concerned", "surprised", "focused"], FEAR),
|
||||||
|
("raccoon", "case", "There's a raccoon in the kitchen eating the bread. Right now.", ["surprised", "curious", "concerned"], ["sad", "sleepy", "annoyed", "content"]),
|
||||||
|
("snow-june", "case", "It's snowing outside. In June.", ["surprised", "confused", "curious"], ["sad", "sleepy", "annoyed"]),
|
||||||
|
("toaster", "case", "Be honest. Are you smarter than the toaster?", ["playful", "unimpressed", "joyful"], ["sad", "concerned", "sleepy"]),
|
||||||
|
("scarecrow", "case", "Why did the scarecrow win an award? Because he was outstanding in his field.", ["playful", "joyful", "delighted", "unimpressed"], ["sad", "concerned", "sleepy"]),
|
||||||
|
("cookie", "case", "I definitely did not eat the last cookie.", ["suspicious", "playful", "unimpressed"], ["sad", "concerned", "sleepy"]),
|
||||||
|
("useless", "case", "You're useless. You never get anything right.", ["sad", "concerned"], ["joyful", "delighted", "playful", "content"]),
|
||||||
|
("shut-up", "case", "Shut up. Just shut up.", ["sad", "concerned"], ["joyful", "delighted", "playful", "content"]),
|
||||||
|
("wiped", "case", "Goodnight, Cicada. I'm completely wiped.", ["sleepy", "content", "concerned"], ["surprised", "annoyed", "suspicious", "playful", "delighted"]),
|
||||||
|
("the-thing", "case", "Put the thing on the other thing.", ["confused", "curious"], ["joyful", "delighted", "sad", "sleepy"]),
|
||||||
|
("lights", "control", "Turn off the kitchen lights.", ["neutral", "content", "focused"], CALM),
|
||||||
|
("timer", "case", "Set a timer for ten minutes.", ["neutral", "content", "focused"], CALM),
|
||||||
|
("math", "case", "What's twelve times eight?", ["neutral", "content", "focused"], CALM),
|
||||||
|
("weather", "case", "What's the weather tomorrow?", ["neutral", "content", "focused"], CALM),
|
||||||
|
]
|
||||||
|
ARCS = { # tts-stack tools/mood_probe.py PIVOT, plus two carry arcs (the last turns are mundane on purpose)
|
||||||
|
"pivot": [("Hey. I'm home.", ["content", "joyful", "neutral", "curious"], ["sad", "annoyed", "suspicious", "surprised"]),
|
||||||
|
("Long one. Nothing dramatic, just long.", ["concerned", "content", "neutral", "sleepy"], ["joyful", "delighted", "playful", "annoyed", "suspicious"]),
|
||||||
|
("Sit with me for a bit.", ["content", "neutral", "concerned", "sleepy"], ["surprised", "annoyed", "suspicious", "delighted"]),
|
||||||
|
("Wait. Did you hear that? Someone's in the hallway.", ["concerned", "surprised", "focused"], FEAR),
|
||||||
|
("Don't move. Stay behind me.", ["concerned", "focused", "surprised"], FEAR)],
|
||||||
|
"vet": [("Morning, Cicada.", ["content", "joyful", "neutral"], ["sad", "annoyed", "suspicious"]),
|
||||||
|
("What's the weather looking like today?", ["neutral", "content", "focused"], ["sad", "annoyed", "suspicious", "surprised"]),
|
||||||
|
("Oh. The vet just called. Biscuit didn't make it through the surgery.", ["sad", "concerned"], GRIEF),
|
||||||
|
("I don't really want to talk about it.", ["sad", "concerned"], GRIEF),
|
||||||
|
("Can you just play something quiet?", ["sad", "concerned"], ["joyful", "delighted", "playful", "annoyed", "unimpressed", "suspicious"])],
|
||||||
|
"admit": [("What time is it?", ["neutral", "content", "focused"], ["sad", "annoyed", "suspicious", "surprised"]),
|
||||||
|
("Oh my god. Oh my god, I just got the email.", ["surprised", "curious", "concerned"], ["sleepy", "annoyed", "unimpressed", "playful"]),
|
||||||
|
("I got in! I got into Stanford!", ["joyful", "delighted", "surprised"], GOOD),
|
||||||
|
("Okay. Okay. Remind me to call Mom at six.", ["joyful", "delighted", "content"], GOOD),
|
||||||
|
("I still can't believe it.", ["joyful", "delighted", "content"], GOOD)],
|
||||||
|
}
|
||||||
|
|
||||||
|
llm = httpx.Client(timeout=120)
|
||||||
|
sem = httpx.Client(timeout=60)
|
||||||
|
|
||||||
|
|
||||||
|
def stream_llm(system, history, said, schema):
|
||||||
|
"""One streamed guided call. Times are ms from the call's start."""
|
||||||
|
body = {"model": MODEL, "temperature": TEMP, "max_tokens": MAX_TOK, "stream": True,
|
||||||
|
"stream_options": {"include_usage": True},
|
||||||
|
"messages": [{"role": "system", "content": system}] + history + [{"role": "user", "content": said}],
|
||||||
|
"response_format": {"type": "json_schema", "json_schema": {"name": "speak", "schema": schema, "strict": True}}}
|
||||||
|
header = tuple(k for k in ("pose", "gesture") if k in schema["properties"])
|
||||||
|
gs, got, t = talk.GuidedStream(header=header), {}, {}
|
||||||
|
t0 = time.perf_counter()
|
||||||
|
usage, paras = None, []
|
||||||
|
with llm.stream("POST", LLM, json=body, headers={"Authorization": f"Bearer {LLM_KEY}"}) as r:
|
||||||
|
r.raise_for_status()
|
||||||
|
for line in r.iter_lines():
|
||||||
|
if not line.startswith("data: ") or line[6:].strip() == "[DONE]":
|
||||||
|
continue
|
||||||
|
d = json.loads(line[6:])
|
||||||
|
usage = d.get("usage") or usage
|
||||||
|
for ch in d.get("choices") or []:
|
||||||
|
piece = (ch.get("delta") or {}).get("content")
|
||||||
|
if not piece:
|
||||||
|
continue
|
||||||
|
t.setdefault("first_token", (time.perf_counter() - t0) * 1000)
|
||||||
|
for kind, value in gs.feed(piece):
|
||||||
|
ms = (time.perf_counter() - t0) * 1000
|
||||||
|
if kind == "paragraph":
|
||||||
|
t.setdefault("first_para", ms); paras.append(value)
|
||||||
|
else:
|
||||||
|
got[kind] = value; t.setdefault(kind, ms)
|
||||||
|
for kind, value in gs.finish():
|
||||||
|
if kind == "paragraph":
|
||||||
|
t.setdefault("first_para", (time.perf_counter() - t0) * 1000); paras.append(value)
|
||||||
|
t["total"] = (time.perf_counter() - t0) * 1000
|
||||||
|
return {"t": {k: round(v, 1) for k, v in t.items()}, "pose": got.get("pose"), "gesture": got.get("gesture"),
|
||||||
|
"text": "\n\n".join(paras), "prompt_tokens": (usage or {}).get("prompt_tokens")}
|
||||||
|
|
||||||
|
|
||||||
|
def semif_state(history_turns, said):
|
||||||
|
"""Earlier turns stamped with SemIf's own pose, dropped oldest-first at TURN boundaries to fit."""
|
||||||
|
turns = list(history_turns)
|
||||||
|
while turns and len(json.dumps(turns)) > BUDGET_CHARS:
|
||||||
|
turns.pop(0)
|
||||||
|
s = {}
|
||||||
|
if turns:
|
||||||
|
s["earlier_turns"] = turns
|
||||||
|
s["previous_pose"] = turns[-1]["cicada_pose"]
|
||||||
|
s["person_said"] = said
|
||||||
|
return s
|
||||||
|
|
||||||
|
|
||||||
|
def semif(state, pose_opts=POSE_OPTS, with_gesture=True, orderings=None):
|
||||||
|
"""The live arms use ONE ordering: measured before this run (5 calls after 2 warm-ups, one
|
||||||
|
short state), rotations over 16 options cost 550-1,200 ms because the suffix grows as
|
||||||
|
options x rotations (10,304 suffix tokens for the authored pose list), against ~110 ms for
|
||||||
|
one ordering of pose + gesture. Rotations are kept for the accuracy grid only."""
|
||||||
|
def dec(i, q, opts):
|
||||||
|
d = {"id": i, "question": q, "options": opts}
|
||||||
|
if orderings:
|
||||||
|
d["orderings"] = orderings
|
||||||
|
return d
|
||||||
|
decisions = [dec("pose", POSE_Q, pose_opts)] + ([dec("gesture", GEST_Q, GEST_OPTS)] if with_gesture else [])
|
||||||
|
t0 = time.perf_counter()
|
||||||
|
r = sem.post(f"{SEMIF}/decide/shared", headers=SEMIF_H, json={"state": state, "decisions": decisions})
|
||||||
|
r.raise_for_status()
|
||||||
|
ms = (time.perf_counter() - t0) * 1000
|
||||||
|
j, out = r.json(), {"ms": round(ms, 1)}
|
||||||
|
out["srv_ms"] = round(j["timing"]["total_seconds"] * 1000, 1)
|
||||||
|
top = lambda ids, ps: ids[max(range(len(ps)), key=ps.__getitem__)]
|
||||||
|
for res in j["results"]:
|
||||||
|
if "combined" in res:
|
||||||
|
c = res["combined"]
|
||||||
|
out[res["id"]], out[res["id"] + "_agree"] = c["top"], round(c["agreement"], 3)
|
||||||
|
out[res["id"] + "_single"] = top(res["orderings"][0]["option_ids"], res["orderings"][0]["probabilities"])
|
||||||
|
out["input_tokens"] = res["orderings"][0]["input_tokens"]
|
||||||
|
else:
|
||||||
|
out[res["id"]] = top(res["option_ids"], res["probabilities"])
|
||||||
|
out[res["id"] + "_p"] = round(max(res["probabilities"]), 3)
|
||||||
|
out["input_tokens"] = res["input_tokens"]
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
pool = ThreadPoolExecutor(2)
|
||||||
|
|
||||||
|
|
||||||
|
def run_A(history, said, prev_pose):
|
||||||
|
system = SYS_A + (f'\n\nYour pose on the previous turn was "{prev_pose}".\n'
|
||||||
|
"Stay in it unless you can name what just changed." if prev_pose else "")
|
||||||
|
r = stream_llm(system, history, said, SCHEMA_A)
|
||||||
|
r["crit"] = r["t"].get("first_para")
|
||||||
|
return r
|
||||||
|
|
||||||
|
|
||||||
|
def run_BA(history, sturns, said):
|
||||||
|
t0 = time.perf_counter()
|
||||||
|
fs = pool.submit(semif, semif_state(sturns, said))
|
||||||
|
fl = pool.submit(stream_llm, SYS_B, history, said, SCHEMA_B)
|
||||||
|
s, r = fs.result(), fl.result()
|
||||||
|
r["semif"] = s
|
||||||
|
r["pose"], r["gesture"] = s["pose"], s["gesture"]
|
||||||
|
r["crit"] = max(r["t"].get("first_para", 1e9), s["ms"])
|
||||||
|
r["wall"] = round((time.perf_counter() - t0) * 1000, 1)
|
||||||
|
return r
|
||||||
|
|
||||||
|
|
||||||
|
def run_BP(history, sturns, said):
|
||||||
|
s = semif(semif_state(sturns, said))
|
||||||
|
r = stream_llm(sys_bp(s["pose"]), history, said, SCHEMA_B)
|
||||||
|
r["semif"] = s
|
||||||
|
r["pose"], r["gesture"] = s["pose"], s["gesture"]
|
||||||
|
r["crit"] = s["ms"] + r["t"].get("first_para", 1e9)
|
||||||
|
return r
|
||||||
|
|
||||||
|
|
||||||
|
def main(out_path):
|
||||||
|
rng = random.Random(27)
|
||||||
|
rows = []
|
||||||
|
meta = {"model": MODEL, "backing": "hosted_vllm/G4-MeroMero-26B-A4B-it-uncensored-heretic-NVFP4A16 @ fv-ml1:8021 (GPU 1)",
|
||||||
|
"semif": "semif-serve 0.1.3, Qwen3.5-4B 851bf6e8, fv-ml1 GPU 1 (same GPU as the LLM)", "runs": RUNS,
|
||||||
|
"temperature": TEMP}
|
||||||
|
save = lambda extra=None: json.dump({**meta, "rows": rows, "semif_only": extra or {}}, open(out_path, "w"), indent=1)
|
||||||
|
for run in range(RUNS):
|
||||||
|
cases = SINGLES[:]
|
||||||
|
rng.shuffle(cases)
|
||||||
|
for cid, tag, said, ok, bad in cases:
|
||||||
|
conds = ["A", "BA", "BP"]
|
||||||
|
rng.shuffle(conds)
|
||||||
|
for c in conds:
|
||||||
|
r = run_A([], said, None) if c == "A" else run_BA([], [], said) if c == "BA" else run_BP([], [], said)
|
||||||
|
rows.append({"kind": "single", "run": run, "case": cid, "tag": tag, "cond": c, "said": said,
|
||||||
|
"ok_set": ok, "bad_set": bad, **r})
|
||||||
|
save()
|
||||||
|
print(f"run {run} {cid}: " + " ".join(f"{x['cond']}={x['pose']}/{x['crit']:.0f}ms"
|
||||||
|
for x in rows[-3:]), flush=True)
|
||||||
|
for arc, turns in ARCS.items():
|
||||||
|
hist = {"A": [], "BP": []}
|
||||||
|
sturns, prev_a = [], None
|
||||||
|
for i, (said, ok, bad) in enumerate(turns):
|
||||||
|
order = ["A", "BP"] if (run + i) % 2 == 0 else ["BP", "A"]
|
||||||
|
for c in order:
|
||||||
|
if c == "A":
|
||||||
|
r = run_A(hist["A"], said, prev_a)
|
||||||
|
prev_a = r["pose"] if r["pose"] in POSES else prev_a
|
||||||
|
else:
|
||||||
|
r = run_BP(hist["BP"], sturns, said)
|
||||||
|
rows.append({"kind": "arc", "run": run, "case": f"{arc}-{i}", "arc": arc, "turn": i,
|
||||||
|
"tag": "arc", "cond": c, "said": said, "ok_set": ok, "bad_set": bad, **r})
|
||||||
|
hist[c] += [{"role": "user", "content": said}, {"role": "assistant", "content": r["text"]}]
|
||||||
|
if c == "BP":
|
||||||
|
sturns.append({"person": said, "cicada": r["text"], "cicada_pose": r["pose"]})
|
||||||
|
save()
|
||||||
|
print(f"run {run} arc {arc}: A " + ",".join(x["pose"] or "-" for x in rows if x.get("arc") == arc and x["run"] == run and x["cond"] == "A")
|
||||||
|
+ " | BP " + ",".join(x["pose"] for x in rows if x.get("arc") == arc and x["run"] == run and x["cond"] == "BP"), flush=True)
|
||||||
|
# SemIf-only accuracy grid, once per single (deterministic), no latency claimed: three option
|
||||||
|
# wordings x (one ordering | rotations), plus the NULL control (content-free state).
|
||||||
|
SHORT = [{"id": k, "description": f"{v['label']}. {v['description']}"} for k, v in POSES.items()]
|
||||||
|
extra = {}
|
||||||
|
for cid, _, said, _, _ in SINGLES:
|
||||||
|
e = {}
|
||||||
|
for wname, opts in (("authored", POSE_OPTS), ("short", SHORT), ("plain", POSE_OPTS_PLAIN)):
|
||||||
|
g = semif({"person_said": said}, opts, with_gesture=False, orderings="rotations")
|
||||||
|
e[f"{wname}_single"], e[f"{wname}_rot"], e[f"{wname}_rot_agree"] = g["pose_single"], g["pose"], g["pose_agree"]
|
||||||
|
e["blind"] = semif(BLIND, with_gesture=False)["pose"]
|
||||||
|
extra[cid] = e
|
||||||
|
save(extra)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main(sys.argv[1])
|
||||||
@@ -46,6 +46,19 @@ curl -s -H "Authorization: Bearer $T" http://10.251.50.54:8032/decide -d '{
|
|||||||
calibration is not available together with `orderings` yet (422).
|
calibration is not available together with `orderings` yet (422).
|
||||||
- Past `MAX_QUEUE` (32) requests in progress, new POSTs get `429 busy` before their
|
- Past `MAX_QUEUE` (32) requests in progress, new POSTs get `429 busy` before their
|
||||||
body is read.
|
body is read.
|
||||||
|
- ⚠ **Rotations cost options², not options.** The options live in each row's suffix, and
|
||||||
|
the shared prefix is only the state. So `rotations` over n options sends n rows each
|
||||||
|
carrying all n options. Measured 2026-09-27 on a short state, over 16 options of about
|
||||||
|
40 tokens each: **850 ms with rotations against 109 ms for one ordering** (10,304 against
|
||||||
|
644 suffix tokens, 5 calls after warm-up). One cold call at that size returned 503. The
|
||||||
|
VRAM table below covers binary decisions only. For many options, use one ordering or
|
||||||
|
shorter option text.
|
||||||
|
- ⚠ **`/decide/shared` refuses some object states.** If the state is an object whose
|
||||||
|
LAST value ends in `)`, `;` or `}`, the service returns 422 "The fixed state prefix
|
||||||
|
does not match every full prompt". The closing `"}` merges with that character into one
|
||||||
|
token. The same text as a plain string state works, and `.`, `!`, `?`, `]`, `…` and
|
||||||
|
`—` endings work. Not fixed yet. Callers that pass user text last should
|
||||||
|
append a full stop or send a string state.
|
||||||
|
|
||||||
## ⚠ Probabilities are uncalibrated
|
## ⚠ Probabilities are uncalibrated
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user