openvibers/openvibe.live Commits
Arena: a beef hit needs the target's name in the lines judged; what follows a name-drop is context only (a League stream's voice lines after one transcribed "Adam" scored a 107-0 beef, 2026-10-18) (#245)
Changes
@@@ -48,7 +48,7 @@ Every 15 s, for every roster stream that is live **and** has a transcript line i1. New speech lines are read.2. **Name detection** (`server/arena/names.js`) — a fighter is named only when their **whole** username or display name is said as it is spelled (camelCase/snake split, leet undone and spacing are fine: "mnss 22741", "grizzly bear"); pieces of a handle ("fix it" of finditfixit, "mnss"), nicknames (fighter name, `spoken_as`, a handle's first word) and fuzzy or phonetic sound-alikes never count (`findMentions(…, { wholeOnly: true })`). On 2026-10-18, 46 of the first 51 beefs had no whole name said (a League stream's "Minions" had matched mnss22741 phonetically); they were voided (`status = 'void'`, out of every record). The speaker never matches themselves.--3. **Focus lock** — a name-drop locks the ears on that fighter for 2 min; everything said afterwards goes to the **beef judge** with the context of what was already said (`about_target`, `aimed_at_target`, quality, best verbatim line, announcer call). A hit → `beef.recordHit()`; every hit extends the lock 3 min (cap 20 min without a fresh name-drop); two off-target chunks drop it; a different name switches targets.++3. **Focus lock** — a name-drop locks the ears on that fighter for 2 min; everything said afterwards goes to the **beef judge** with the context of what was already said (`about_target`, `aimed_at_target`, quality, best verbatim line, announcer call). A hit, only in lines that say the target's name (what follows is context: a game's voice lines after one transcribed "Adam" once scored 107–0), → `beef.recordHit()`; every hit extends the lock 3 min (cap 20 min without a fresh name-drop); two off-target chunks drop it; a different name switches targets.4. **Free talk** — lines said while not locked pool for the **mic judge** (≥ 20 words, ≥ 30 s between calls per stream): `is_trash_talk`, quality, best line, `aimed_at`, announcer. Quality ≥ 4 → a `trash` moment in the feed. An `aimed_at` that is a roster fighter is dropped (nobody named them; a whole name said would have locked the ears instead), so free talk never opens a beef.5. One judge call per stream per tick; keyword heuristics when AI is off.@@@ -5,9 +5,10 @@* lines. Everything the Arena is comes from here; chat, votes and submissions do not exist.** name-drop → a line that says another fighter's whole name, as it is spelled (names.js-- * wholeOnly: no sound-alikes, no pieces of a handle) LOCKS the ears on that fighter. Everything said while locked goes to-- * the BEEF JUDGE with the earlier context; a judged hit → beef.recordHit() (opens-- * the beef, scores it, puts the other side on the clock). See beef.js.++ * wholeOnly: no sound-alikes, no pieces of a handle) LOCKS the ears on that fighter.++ * Everything said while locked goes to the BEEF JUDGE with the earlier context; a++ * judged hit in lines that say the name → beef.recordHit() (opens the beef, scores++ * it, puts the other side on the clock); unnamed lines are context only. See beef.js.* free talk → everything said while NOT locked pools for the MIC JUDGE: is this shit talk at* all (roast, rant, callout, brag, disrespect)? Who is it aimed at ("chat", "the* mods"; a fighter the judge guesses at is dropped, since nobody named them)? It@@@ -174,7 +175,9 @@ async function judgeFocus(stream, roster, st, events, { reason }) {const j = await judgeBeef(stream.user_id, f.targetId, text, roster, { context: f.context, named, how: f.how });const now = Date.now();const minQ = f.hits > 0 ? 3 : (f.how === 'exact' ? 3 : 5);-- if (j.aimed_at_target && j.quality >= minQ) {++ // A hit needs the target's name in the lines judged: what follows a name-drop is context, never a hit by itself++ // (a League stream's voice lines after one transcribed "Adam" scored a 107–0 beef, 2026-10-18).++ if (j.aimed_at_target && j.quality >= minQ && named) {const ref = lineRefFor(lines, j.best_line);const res = await beef().recordHit(stream.user_id, f.targetId, { quality: j.quality, best_line: j.best_line, about: j.about, announcer: j.announcer, vod_id: ref.vod_id, sec: ref.sec, stream_id: stream.id });f.hits++; f.misses = 0; f.lockUntil = Math.min(now + FOCUS_EXTEND_MS, f.namedAt + FOCUS_MAX_MS);@@@ -129,7 +129,14 @@ console.log('✅ roster + ratings are pure mic');assert.ok((await listener.consoleState(u3)).focus && (await listener.consoleState(u3)).focus.target_id === u2, 'locked on grizzly');assert.strictEqual((await beef.openBeefsFor(u3)).length, 1);assert.strictEqual((await arena.liveFighters())[0].ears.focus.target_id, u2, 'live cam shows the lock');-- // Drop the lock (two off-target chunks), then a callout WITHOUT the name in the speech but with the judge's aimed_at.++ // Unnamed trash while locked is context, never a hit (a League stream's voice lines after one transcribed "Adam"++ // scored a 107–0 beef, 2026-10-18).++ await db.run(`UPDATE streams SET started_at = datetime('now', '-760 seconds') WHERE id = ?`, [liveId]);++ await db.addTimelineEvents([{ stream_id: liveId, user_id: u3, vod_id: null, kind: 'speech', start_sec: 746, end_sec: 752, text: 'He is trash, washed, a fraud, scared of the smoke, nobody watches that garbage stream, I would cook him, bet!', label: null, confidence: 0.9 }]);++ listener._state.get(liveId).lastJudgeAt = 0;++ ev = await listener.tick();++ assert.ok(!ev.some(e => e.kind === 'beef_hit'), `unnamed lines never score: ${JSON.stringify(ev)}`);++ // Drop the lock (two off-target chunks).for (let k = 0; k < 2; k++) {await db.run(`UPDATE streams SET started_at = datetime('now', ?) WHERE id = ?`, [`-${800 + k * 100} seconds`, liveId]);await db.addTimelineEvents([{ stream_id: liveId, user_id: u3, vod_id: null, kind: 'speech', start_sec: 786 + k * 100, end_sec: 792 + k * 100, text: 'Okay chat back to the game, we need to farm this boss and then do the quest line for the sword upgrade, let me check the map real quick.', label: null, confidence: 0.9 }]);
@@ -48,7 +48,7 @@ Every 15 s, for every roster stream that is live **and** has a transcript line i
@@ -48,7 +48,7 @@ Every 15 s, for every roster stream that is live **and** has a transcript line i
1. New speech lines are read.
1. New speech lines are read.
2. **Name detection** (`server/arena/names.js`) — a fighter is named only when their **whole** username or display name is said as it is spelled (camelCase/snake split, leet undone and spacing are fine: "mnss 22741", "grizzly bear"); pieces of a handle ("fix it" of finditfixit, "mnss"), nicknames (fighter name, `spoken_as`, a handle's first word) and fuzzy or phonetic sound-alikes never count (`findMentions(…, { wholeOnly: true })`). On 2026-10-18, 46 of the first 51 beefs had no whole name said (a League stream's "Minions" had matched mnss22741 phonetically); they were voided (`status = 'void'`, out of every record). The speaker never matches themselves.
2. **Name detection** (`server/arena/names.js`) — a fighter is named only when their **whole** username or display name is said as it is spelled (camelCase/snake split, leet undone and spacing are fine: "mnss 22741", "grizzly bear"); pieces of a handle ("fix it" of finditfixit, "mnss"), nicknames (fighter name, `spoken_as`, a handle's first word) and fuzzy or phonetic sound-alikes never count (`findMentions(…, { wholeOnly: true })`). On 2026-10-18, 46 of the first 51 beefs had no whole name said (a League stream's "Minions" had matched mnss22741 phonetically); they were voided (`status = 'void'`, out of every record). The speaker never matches themselves.
-3. **Focus lock** — a name-drop locks the ears on that fighter for 2 min; everything said afterwards goes to the **beef judge** with the context of what was already said (`about_target`, `aimed_at_target`, quality, best verbatim line, announcer call). A hit → `beef.recordHit()`; every hit extends the lock 3 min (cap 20 min without a fresh name-drop); two off-target chunks drop it; a different name switches targets.
+3. **Focus lock** — a name-drop locks the ears on that fighter for 2 min; everything said afterwards goes to the **beef judge** with the context of what was already said (`about_target`, `aimed_at_target`, quality, best verbatim line, announcer call). A hit, only in lines that say the target's name (what follows is context: a game's voice lines after one transcribed "Adam" once scored 107–0), → `beef.recordHit()`; every hit extends the lock 3 min (cap 20 min without a fresh name-drop); two off-target chunks drop it; a different name switches targets.
4. **Free talk** — lines said while not locked pool for the **mic judge** (≥ 20 words, ≥ 30 s between calls per stream): `is_trash_talk`, quality, best line, `aimed_at`, announcer. Quality ≥ 4 → a `trash` moment in the feed. An `aimed_at` that is a roster fighter is dropped (nobody named them; a whole name said would have locked the ears instead), so free talk never opens a beef.
4. **Free talk** — lines said while not locked pool for the **mic judge** (≥ 20 words, ≥ 30 s between calls per stream): `is_trash_talk`, quality, best line, `aimed_at`, announcer. Quality ≥ 4 → a `trash` moment in the feed. An `aimed_at` that is a roster fighter is dropped (nobody named them; a whole name said would have locked the ears instead), so free talk never opens a beef.
5. One judge call per stream per tick; keyword heuristics when AI is off.
5. One judge call per stream per tick; keyword heuristics when AI is off.
@@ -5,9 +5,10 @@
@@ -5,9 +5,10 @@
* lines. Everything the Arena is comes from here; chat, votes and submissions do not exist.
* lines. Everything the Arena is comes from here; chat, votes and submissions do not exist.
*
*
* name-drop → a line that says another fighter's whole name, as it is spelled (names.js
* name-drop → a line that says another fighter's whole name, as it is spelled (names.js
- * wholeOnly: no sound-alikes, no pieces of a handle) LOCKS the ears on that fighter. Everything said while locked goes to
- * the BEEF JUDGE with the earlier context; a judged hit → beef.recordHit() (opens
- * the beef, scores it, puts the other side on the clock). See beef.js.
+ * wholeOnly: no sound-alikes, no pieces of a handle) LOCKS the ears on that fighter.
+ * Everything said while locked goes to the BEEF JUDGE with the earlier context; a
+ * judged hit in lines that say the name → beef.recordHit() (opens the beef, scores
+ * it, puts the other side on the clock); unnamed lines are context only. See beef.js.
* free talk → everything said while NOT locked pools for the MIC JUDGE: is this shit talk at
* free talk → everything said while NOT locked pools for the MIC JUDGE: is this shit talk at
* all (roast, rant, callout, brag, disrespect)? Who is it aimed at ("chat", "the
* all (roast, rant, callout, brag, disrespect)? Who is it aimed at ("chat", "the
* mods"; a fighter the judge guesses at is dropped, since nobody named them)? It
* mods"; a fighter the judge guesses at is dropped, since nobody named them)? It
@@ -174,7 +175,9 @@ async function judgeFocus(stream, roster, st, events, { reason }) {
@@ -174,7 +175,9 @@ async function judgeFocus(stream, roster, st, events, { reason }) {
const j = await judgeBeef(stream.user_id, f.targetId, text, roster, { context: f.context, named, how: f.how });
const j = await judgeBeef(stream.user_id, f.targetId, text, roster, { context: f.context, named, how: f.how });
const now = Date.now();
const now = Date.now();
const minQ = f.hits > 0 ? 3 : (f.how === 'exact' ? 3 : 5);
const minQ = f.hits > 0 ? 3 : (f.how === 'exact' ? 3 : 5);
- if (j.aimed_at_target && j.quality >= minQ) {
+ // A hit needs the target's name in the lines judged: what follows a name-drop is context, never a hit by itself
+ // (a League stream's voice lines after one transcribed "Adam" scored a 107–0 beef, 2026-10-18).
+ if (j.aimed_at_target && j.quality >= minQ && named) {
const ref = lineRefFor(lines, j.best_line);
const ref = lineRefFor(lines, j.best_line);
const res = await beef().recordHit(stream.user_id, f.targetId, { quality: j.quality, best_line: j.best_line, about: j.about, announcer: j.announcer, vod_id: ref.vod_id, sec: ref.sec, stream_id: stream.id });
const res = await beef().recordHit(stream.user_id, f.targetId, { quality: j.quality, best_line: j.best_line, about: j.about, announcer: j.announcer, vod_id: ref.vod_id, sec: ref.sec, stream_id: stream.id });
f.hits++; f.misses = 0; f.lockUntil = Math.min(now + FOCUS_EXTEND_MS, f.namedAt + FOCUS_MAX_MS);
f.hits++; f.misses = 0; f.lockUntil = Math.min(now + FOCUS_EXTEND_MS, f.namedAt + FOCUS_MAX_MS);
@@ -129,7 +129,14 @@ console.log('✅ roster + ratings are pure mic');
@@ -129,7 +129,14 @@ console.log('✅ roster + ratings are pure mic');
assert.ok((await listener.consoleState(u3)).focus && (await listener.consoleState(u3)).focus.target_id === u2, 'locked on grizzly');
assert.ok((await listener.consoleState(u3)).focus && (await listener.consoleState(u3)).focus.target_id === u2, 'locked on grizzly');
assert.strictEqual((await beef.openBeefsFor(u3)).length, 1);
assert.strictEqual((await beef.openBeefsFor(u3)).length, 1);
assert.strictEqual((await arena.liveFighters())[0].ears.focus.target_id, u2, 'live cam shows the lock');
assert.strictEqual((await arena.liveFighters())[0].ears.focus.target_id, u2, 'live cam shows the lock');
- // Drop the lock (two off-target chunks), then a callout WITHOUT the name in the speech but with the judge's aimed_at.
+ // Unnamed trash while locked is context, never a hit (a League stream's voice lines after one transcribed "Adam"
+ // scored a 107–0 beef, 2026-10-18).
+ await db.run(`UPDATE streams SET started_at = datetime('now', '-760 seconds') WHERE id = ?`, [liveId]);
+ await db.addTimelineEvents([{ stream_id: liveId, user_id: u3, vod_id: null, kind: 'speech', start_sec: 746, end_sec: 752, text: 'He is trash, washed, a fraud, scared of the smoke, nobody watches that garbage stream, I would cook him, bet!', label: null, confidence: 0.9 }]);
+ listener._state.get(liveId).lastJudgeAt = 0;
+ ev = await listener.tick();
+ assert.ok(!ev.some(e => e.kind === 'beef_hit'), `unnamed lines never score: ${JSON.stringify(ev)}`);
+ // Drop the lock (two off-target chunks).
for (let k = 0; k < 2; k++) {
for (let k = 0; k < 2; k++) {
await db.run(`UPDATE streams SET started_at = datetime('now', ?) WHERE id = ?`, [`-${800 + k * 100} seconds`, liveId]);
await db.run(`UPDATE streams SET started_at = datetime('now', ?) WHERE id = ?`, [`-${800 + k * 100} seconds`, liveId]);
await db.addTimelineEvents([{ stream_id: liveId, user_id: u3, vod_id: null, kind: 'speech', start_sec: 786 + k * 100, end_sec: 792 + k * 100, text: 'Okay chat back to the game, we need to farm this boss and then do the quest line for the sword upgrade, let me check the map real quick.', label: null, confidence: 0.9 }]);
await db.addTimelineEvents([{ stream_id: liveId, user_id: u3, vod_id: null, kind: 'speech', start_sec: 786 + k * 100, end_sec: 792 + k * 100, text: 'Okay chat back to the game, we need to farm this boss and then do the quest line for the sword upgrade, let me check the map real quick.', label: null, confidence: 0.9 }]);