feat(orchestrator): S3 generated SVG faces whose mouths move while speaking

Requirement 001 phase 6, S3:

- faces.js derives a face from the agent's stable key (its session id,
  or name:<name> without a session), so a rename keeps the face: shape
  4, colour 7, eyes 5, brows 4, mouth 5, hair or headwear 6, accessory 5,
  84,000 combinations. It emits constant SVG tags and attributes only.
- The emoji avatar is gone: popup, Orbit and Chord nodes, the radio
  view, stream and palette draw the SVG face (the 3D views keep their
  marker).
- While the radio reads an agent's message, its faces carry the class
  speaking and the mouth runs the talk keyframes; a silent face does
  not move, and prefers-reduced-motion stops the mouth.

Evidence: make test 673 OK, ruff and markdownlint clean
(/tmp/s3-make-test-1.log); e2e 20 OK in Chromium and Firefox, none
skipped (/tmp/s3-e2e-3.log); screenshots
/tmp/orchestrator-phase4-shots/*-speaking.png; MEDUSA 13 files, 2
findings ("PLA" in "templates", the UI call openAgent) accepted after
an independent skeptic that found no markup sink or selector injection
in the face code.

🎯 Fix: 94 %
💥 Break: 3 %
💸 Cost: 100 %
⏱️ Time: –
🛡️ Security: 100 %
🏆 Score: 91
🚦 Gate: ✅ 90/5, cost and time <= 100, security 100 %

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Kevin Veen-BirkenbachandClaude Opus 5.5 committed 2026-11-08 22:06:31 +00:00
1 parent d89fb616b8
commit 4d88296e47
13 files changed
+262 -34

No files matched your search

+42 -8
View File
@@ -243,7 +243,9 @@ POPUP_ORBIT = """(async () => {@HELPERS@@POPUPS@
const flat = !tall();
click(pick());
await sleep(400);
const shown = {...opened(), ...look(pick()), sections: all("h3", box).map((head) => head.firstChild.textContent), composer: Boolean(box.querySelector("form textarea")), offices: text(box.querySelector(".popup-body p"))};
const art = box.querySelector(".popup-head .avatar.big");
const face = art ? {key: art.dataset.face, svg: Boolean(art.querySelector("svg.face-art .mouth")), text: art.textContent} : null;
const shown = {...opened(), ...look(pick()), sections: all("h3", box).map((head) => head.firstChild.textContent), composer: Boolean(box.querySelector("form textarea")), offices: text(box.querySelector(".popup-body p")), face};
const gone = await away(pick);
click(pick());
await sleep(300);
@@ -491,12 +493,18 @@ SPEAKING = """
return node && !node.hidden ? node : null;
};
const ring = (chip) => (chip ? getComputedStyle(chip.querySelector(".speaker-face"), "::after").animationName : null);
const mouth = (art) => (art ? [art.classList.contains("speaking"), getComputedStyle(art.querySelector(".mouth")).animationName] : null);
const faces = () => ({
speaking: mouth(document.querySelector('#main .speakers .avatar[data-face="s2"]')),
silent: mouth(document.querySelector('#main .speakers .avatar[data-face="s1"]')),
keys: [...new Set(all("#main .avatar.speaking").map((art) => art.dataset.face))],
});
const cue = async (word) => {
await until(() => document.querySelector("#main .speakers .speaker"), 8000);
await fetch("/api/tts", {method: "POST", headers: {"X-Kvb-Orchestrator": "1", "Content-Type": "application/json"}, body: JSON.stringify({text: word, voice: ""}), cache: "no-store"});
const found = await until(caption, 25000);
const chip = document.querySelector("#main .speaker.speaking");
return found ? {who: text(found.querySelector("strong")), line: text(found.querySelector(".caption-line")), chip: chip ? chip.dataset.key : null, ring: ring(chip)} : {error: "no caption", toasts: all("#toasts .toast").map(text)};
return found ? {who: text(found.querySelector("strong")), line: text(found.querySelector(".caption-line")), chip: chip ? chip.dataset.key : null, ring: ring(chip), faces: faces()} : {error: "no caption", toasts: all("#toasts .toast").map(text)};
};
"""
SPEAK = """(async () => {@HELPERS@@SPEAKING@
@@ -505,14 +513,18 @@ SPEAK = """(async () => {@HELPERS@@SPEAKING@
SPEAK_GRAPH = """(async () => {@HELPERS@@SPEAKING@
location.hash = "#agents/orbit";
const node = await until(() => document.querySelector("#main svg .node.speaking"), 6000);
return {node: node ? node.getAttribute("aria-label") : null, ring: node ? getComputedStyle(node.querySelector("circle")).animationName : null};
const silent = document.querySelector('#main svg .node[data-pick="s1"] .mouth');
return {
node: node ? node.getAttribute("aria-label") : null, ring: node ? getComputedStyle(node.querySelector("circle")).animationName : null,
mouth: node ? getComputedStyle(node.querySelector(".mouth")).animationName : null, silent: silent ? getComputedStyle(silent).animationName : null,
};
})()""".replace("@HELPERS@", HELPERS).replace("@SPEAKING@", SPEAKING)
SPEAK_END = """(async () => {@HELPERS@@SPEAKING@
const ended = Boolean(await until(() => !document.querySelector("#main svg .node") || !document.querySelector("#main svg .node.speaking"), 20000));
location.hash = "#stream/radio";
await until(() => document.querySelector("#main .radio-caption"), 6000);
await sleep(300);
return {ended, caption: Boolean(caption()), chips: all("#main .speaker.speaking").length};
return {ended, caption: Boolean(caption()), chips: all("#main .speaker.speaking").length, faces: all("#main .avatar.speaking").length};
})()""".replace("@HELPERS@", HELPERS).replace("@SPEAKING@", SPEAKING)
COMPOSE = """(async () => {@HELPERS@
const form = await until(() => document.querySelector("#main form.composer"), 8000);
@@ -908,6 +920,7 @@ class TestOrchestratorE2E(HubCase):
self.assertNotIn("error", artifact)
self.assertTrue(agent["flat"], "#agents/orbit scrolls")
self.popped(agent, "Stage")
self.assertEqual(agent["shown"]["face"], {"key": "s2", "svg": True, "text": ""})
self.assertEqual(agent["shown"]["sections"], ["Tasks", "Claims", "Subagents", "History"])
self.assertTrue(agent["shown"]["composer"])
self.assertRegex(agent["shown"]["offices"], r"^Offices: .*identity/oidc")
@@ -1079,11 +1092,32 @@ class TestOrchestratorE2E(HubCase):
(OUT / "chromium-speaking.json").write_text(json.dumps(data, indent=1), encoding="utf-8")
self.assertEqual(data["errors"], [])
speaking, graph, ended, calm = data["results"]
self.assertEqual((speaking["who"], speaking["line"], speaking["chip"], speaking["ring"]), ("Stage speaking", "Disk almost full (e2e-cue-1)", "s2", "speaking"), speaking)
self.assertEqual((graph["node"] or "").split(",")[0], "Stage", graph)
self.assertEqual(graph["ring"], "speaking-node")
self.assertEqual(ended, {"ended": True, "caption": False, "chips": 0})
self.talks(speaking, graph)
self.assertEqual(ended, {"ended": True, "caption": False, "chips": 0, "faces": 0})
self.assertEqual((calm["who"], calm["line"], calm["ring"]), ("Stage speaking", "Disk almost full (e2e-cue-2)", "none"), calm)
self.assertEqual(calm["faces"]["speaking"], [True, "none"], calm)
@unittest.skipUnless(shutil.which(os.environ.get("FIREFOX", "firefox")), "no Firefox")
def test_firefox_shows_who_speaks_and_moves_only_the_speakers_mouth(self):
self.speaks(("e2e-cue-1",))
steps = [
{"width": 1366, "height": 768, "url": f"{self.base}#stream/radio", "wait": 900, "probe": SPEAK.replace("@CUE@", "e2e-cue-1"), "shot": str(OUT / "firefox-1366-radio-speaking.png"), "gesture": True},
{"width": 1366, "height": 768, "url": None, "wait": 0, "probe": SPEAK_GRAPH, "shot": str(OUT / "firefox-1366-agents-speaking.png")},
]
data = self.drive(steps, None, "bidi.mjs", os.environ.get("FIREFOX", "firefox"))
(OUT / "firefox-speaking.json").write_text(json.dumps(data, indent=1), encoding="utf-8")
self.assertEqual([error for error in data["errors"] if "/favicon.ico" not in error], [])
self.talks(*data["results"])
def talks(self, speaking: dict, graph: dict) -> None:
"""Args:
speaking: the SPEAK probe while Stage (s2) is read aloud.
graph: the SPEAK_GRAPH probe on #agents/orbit right after it.
"""
self.assertEqual((speaking["who"], speaking["line"], speaking["chip"], speaking["ring"]), ("Stage speaking", "Disk almost full (e2e-cue-1)", "s2", "speaking"), speaking)
self.assertEqual(speaking["faces"], {"speaking": [True, "talk"], "silent": [False, "none"], "keys": ["s2"]}, speaking)
self.assertEqual((graph["node"] or "").split(",")[0], "Stage", graph)
self.assertEqual((graph["ring"], graph["mouth"], graph["silent"]), ("speaking-node", "talk", "none"), graph)
def test_chromium_records_transcribes_and_sends_a_signed_call_with_voice(self):
local = f"http://localhost:{self.port}/"
+1
View File
@@ -77,6 +77,7 @@ VOICE = HUB / "templates" / "voice.js"
SPACE = HUB / "templates" / "space.js"
NAV = HUB / "templates" / "nav.js"
POPUP = HUB / "templates" / "popup.js"
FACES = HUB / "templates" / "faces.js"
class FakeScripts:
+94
View File
@@ -0,0 +1,94 @@
from __future__ import annotations
import json
import math
import re
import subprocess
import unittest
from collections import Counter
from hub_support import FACES
FACES_PROBE = """
const F = require(process.argv[1]);
let seed = 7;
const hex = (digits) => Array.from({length: digits}, () => {
seed = (Math.imul(seed, 1664525) + 1013904223) >>> 0;
return (seed >>> 28).toString(16);
}).join("");
const ids = Array.from({length: 5000}, () => `${hex(8)}-${hex(4)}-${hex(4)}-${hex(4)}-${hex(12)}`);
const fixed = ["s1", "s2", "name:Stage", "Operator", "", "名前"];
console.log(JSON.stringify({
size: F.SIZE,
counts: F.COUNTS,
combinations: F.COMBINATIONS,
picks: ids.map((id) => F.of(id).pick),
again: ids.slice(0, 200).every((id) => JSON.stringify(F.of(id)) === JSON.stringify(F.of(id))),
fixed: fixed.map((id) => F.of(id)),
parts: ids.slice(0, 400).map((id) => F.of(id).parts),
}));
"""
ATTRIBUTES = ["shape", "colour", "eyes", "brows", "mouth", "hair", "accessory"]
TAGS = {"ellipse", "rect", "path"}
COLOUR = re.compile(r"^#[0-9a-f]{3}(?:[0-9a-f]{3})?$")
def probe() -> dict:
done = subprocess.run(["node", "-e", FACES_PROBE, str(FACES)], capture_output=True, text=True, timeout=60, check=True)
return json.loads(done.stdout)
class TestFacesModule(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.data = probe()
cls.rerun = probe()
def test_seven_independent_attributes_multiply_to_at_least_1000_combinations(self):
counts = self.data["counts"]
self.assertEqual(list(counts), ATTRIBUTES)
self.assertTrue(all(count >= 2 for count in counts.values()), counts)
self.assertEqual(self.data["combinations"], math.prod(counts.values()))
self.assertGreaterEqual(self.data["combinations"], 1000)
def test_many_ids_reach_at_least_1000_distinct_faces(self):
distinct = {tuple(pick[name] for name in ATTRIBUTES) for pick in self.data["picks"]}
self.assertGreaterEqual(len(distinct), 1000)
self.assertGreater(len(distinct), len(self.data["picks"]) * 0.9)
def test_the_same_id_gets_the_same_face_in_every_call_and_every_run(self):
self.assertTrue(self.data["again"])
self.assertEqual(self.data["fixed"], self.rerun["fixed"])
self.assertEqual(self.data["picks"], self.rerun["picks"])
self.assertEqual(len({json.dumps(face["pick"]) for face in self.data["fixed"]}), len(self.data["fixed"]))
def test_different_ids_spread_evenly_over_every_variant_of_every_attribute(self):
total = len(self.data["picks"])
for name, count in self.data["counts"].items():
seen = Counter(pick[name] for pick in self.data["picks"])
with self.subTest(attribute=name):
self.assertEqual(sorted(seen), list(range(count)))
for variant, hits in seen.items():
self.assertTrue(0.6 < hits * count / total < 1.4, (variant, hits))
def test_the_svg_description_has_a_coloured_head_one_mouth_and_no_missing_values(self):
for parts in [*self.data["parts"], *(face["parts"] for face in self.data["fixed"])]:
with self.subTest(parts=parts[0]):
self.assertEqual({tag for tag, _ in parts} - TAGS, set())
self.assertRegex(parts[0][1]["fill"], COLOUR)
self.assertEqual([attrs.get("class") for _, attrs in parts].count("mouth"), 1)
for _, attrs in parts:
for key, value in attrs.items():
if isinstance(value, str):
self.assertTrue(value and "undefined" not in value and "NaN" not in value, (key, value))
else:
self.assertIsInstance(value, (int, float), (key, value))
self.assertTrue(math.isfinite(value) and -1 <= value <= self.data["size"] + 1, (key, value))
def test_faces_draw_no_circle_because_graph_node_styles_target_every_circle_in_a_node(self):
tags = {tag for parts in self.data["parts"] for tag, _ in parts}
self.assertNotIn("circle", tags)
if __name__ == "__main__":
unittest.main()
+4 -4
View File
@@ -11,7 +11,7 @@ from pathlib import Path
from unittest import mock
from hub_case import HubCase
from hub_support import AMBIENCE, NAV, POPUP, SPACE, VOICE
from hub_support import AMBIENCE, FACES, NAV, POPUP, SPACE, VOICE
from kvb_orchestrator import exchange
@@ -63,9 +63,9 @@ class TestHubPage(HubCase):
self.assertEqual((state["reviews"][0]["title"], state["reviews"][0]["notes"]), ("<script>alert(1)</script>", "</pre><script>alert(2)</script>"))
self.assertEqual((state["register"][0]["title"], state["radio"][0]["from"]), ("<script>alert(3)</script>", "<img src=x>"))
def test_the_index_scripts_parse_and_embed_the_ambience_voice_nav_popup_and_space_modules(self):
def test_the_index_scripts_parse_and_embed_the_ambience_voice_nav_popup_faces_and_space_modules(self):
html = self.index()
for module in (AMBIENCE, VOICE, NAV, POPUP, SPACE):
for module in (AMBIENCE, VOICE, NAV, POPUP, FACES, SPACE):
with self.subTest(module=module.name):
self.assertIn(module.read_text(encoding="utf-8").strip(), html)
scripts = re.findall(r'<script nonce="[^"]+">(.*?)</script>', html, re.DOTALL)
@@ -74,7 +74,7 @@ class TestHubPage(HubCase):
page.write_text("\n;\n".join(scripts), encoding="utf-8")
done = subprocess.run(["node", "--check", str(page)], capture_output=True, text=True, timeout=60, check=False)
self.assertEqual(done.returncode, 0, done.stderr)
for module in (AMBIENCE, VOICE, NAV, POPUP, SPACE):
for module in (AMBIENCE, VOICE, NAV, POPUP, FACES, SPACE):
with self.subTest(module=module.name):
done = subprocess.run(["node", "--check", str(module)], capture_output=True, text=True, timeout=60, check=False)
self.assertEqual(done.returncode, 0, done.stderr)