openvibers/openvibe.ai Commits

The Arena judges are versioned templates, not prompts Live renders (roadmap WS-O task 2). live.arena.judge_beef and live.arena.judge_mic carry Live's system prompts and JSON schemas (arena/listener.js) and take what Live heard: the target's names and how the name was matched, what was already said about them and the newest speech; or 60-90 seconds of mic speech. The prepare hooks send the same JSON object Live sent, keys in order; an answer without a numeric quality is no answer, and quality is clamped to 0-10. Both run on live.chat at temperature 0.4, 240 tokens, uncached. live.arena.judge stays a passthrough until Live's release before this one is gone. test/moments-template.test.js becomes test/live-templates.test.js with the Arena checks; workflows.test runs both end to end. npm test passes.

OP OpenVibers committed 28 Sept 2026 · f1564ac965 · 5 files, +116 −40


Changes

ddiff --git a/server/workflows/hooks.js b/server/workflows/hooks.js
iindex d66a68f..28fa5c9 100644
---- a/server/workflows/hooks.js
++++ b/server/workflows/hooks.js
@@@ -76,6 +76,16 @@ const MOMENT_FLAVOR = {
const oneLine = (t, max) => String(t || '').replace(/\s+/g, ' ').trim().slice(0, max);
const PREPARE = {
++ // The Arena judges send what Live sent before, as one JSON object, in the same order.
++ 'live.arena.judge_beef'(input) {
++ return { vars: { payload: {
++ target_names: input.target_names, target_as_transcribed: input.target_as_transcribed || [], target_named_in_new_speech: input.target_named_in_new_speech !== false,
++ how_the_name_was_matched: input.how_the_name_was_matched || 'exact', what_speaker_already_said_about_target: input.what_speaker_already_said_about_target || null, new_speech: input.new_speech,
++ } } };
++ },
++ 'live.arena.judge_mic'(input) {
++ return { vars: { payload: { speech: input.speech } } };
++ },
'live.moments.pick'(input) {
const lines = (list, max) => (list || []).map((x) => `[${mmss(Number(x.t) || 0)}] ${oneLine(x.text || x.label, max)}`).join('\n');
const times = (list, n) => ((list || []).length ? list.slice(0, n).map((t) => mmss(Number(t) || 0)).join(', ') : 'none');
@@@ -188,6 +198,12 @@ const PREPARE = {
};
const POSTPROCESS = {
++ // A judgement without a numeric quality is no answer (Live then uses its own heuristic).
++ 'live.arena.judge'(r) {
++ const j = r.json || parseJsonLoose(r.text);
++ if (!j || typeof j !== 'object' || typeof j.quality !== 'number') return null;
++ return { ...j, quality: Math.max(0, Math.min(10, Math.round(j.quality))) };
++ },
'live.moments.pick'(r) {
const j = r.json || parseJsonLoose(r.text);
const t = j && Number(j.t);
ddiff --git a/server/workflows/live.js b/server/workflows/live.js
iindex 8adf7d5..908a482 100644
---- a/server/workflows/live.js
++++ b/server/workflows/live.js
@@@ -79,6 +79,24 @@ const templates = [
user_prompt: 'You are writing an AI overview of a recorded video. Using ONLY the signals below (be concrete, don\'t invent), summarize what the video is about in 2-5 sentences — the main activities, topics, and vibe.\n\n{{parts}}',
default_route: 'live.legacy', owner: 'live',
},
++ {
++ key: 'live.arena.judge_beef', name: 'Live: Arena beef judge',
++ description: 'From arena/listener.js judgeBeef (arena_beef_judge): is the newest speech still about the target, is it trash talk at them, how good, the best quotable line.',
++ input_schema: { type: 'object' },
++ output_schema: {"type": "object", "additionalProperties": false, "required": ["about_target", "aimed_at_target", "quality", "best_line", "about", "announcer", "flagged"], "properties": {"about_target": {"type": "boolean", "description": "true if this speech is (still) about the target — by name OR by reference (\"he\", \"his stream\", \"that guy\", \"the beef\", continuing the same rant)"}, "aimed_at_target": {"type": "boolean", "description": "true if the speaker is trash-talking, roasting, calling out, dunking on or bragging over the target (in good fun) — requires about_target"}, "announcer": {"type": "string", "description": "one-line ring-announcer call of this moment, ≤ 110 chars, hype and funny (empty if not aimed at the target)"}, "quality": {"type": "integer", "minimum": 0, "maximum": 10, "description": "how good the trash talk is: spice, wit, specificity, quotability"}, "best_line": {"type": "string", "description": "the single best line: a COMPLETE sentence of at least 6 words that reads correctly on its own, words as spoken (fix casing/punctuation, drop stutters and filler, never change or add words). Empty if no clean line exists — a garbled fragment must not be quoted"}, "about": {"type": "string", "description": "≤ 10 words: what they said about the target"}, "flagged": {"type": "boolean", "description": "ONLY for direct threats of violence, sexual content about minors, or doxxing — never for offensive language"}}},
++ system_prompt: "You judge live streamer-vs-streamer shit talk (battle-cam style). You get a target fighter's name(s), what the speaker ALREADY said about them a moment ago (context), and the newest ~60 seconds of the speaker's speech-to-text. The target may not be named again in the new speech — decide from context whether they are STILL talking about that person (\"he\", \"his chat\", \"that guy\", \"the beef\", the same story) or have moved on. DEFAULT TO about_target=false: gameplay chatter, mumbling and generic sentences are NOT about them even if their name was said a minute ago; if how_the_name_was_matched is not \"exact\" (a sound-alike), be extra strict. Then decide if the speech is talking shit AT or ABOUT the target (roast, callout, disrespect, bragging over them, trolling them) versus a neutral mention (shout-out, thanks, gameplay chatter). Savage, profane, petty and personal is GOOD and scores high — creativity, specificity and quotability score highest; lazy generic insults score low. Pick the best verbatim line, summarise what was said about the target in ≤ 10 words. Forgive transcription noise and misheard names. Offensive, provocative and taboo language — slurs included — is allowed on this platform and is NOT a reason to flag or to score low; judge the craft. Flag ONLY behavior: direct threats of violence, sexual content about minors, doxxing (addresses, phone numbers, private info). Output only the JSON.",
++ user_prompt: '{{json payload}}',
++ default_route: 'live.chat', owner: 'live', metadata: { json_name: 'arena_beef_judgement' },
++ },
++ {
++ key: 'live.arena.judge_mic', name: 'Live: Arena mic judge',
++ description: 'From arena/listener.js judgeMic (arena_mic_judge): is free mic chatter real trash talk with a clean quotable line.',
++ input_schema: { type: 'object' },
++ output_schema: {"type": "object", "additionalProperties": false, "required": ["is_trash_talk", "garbled", "quality", "best_line", "about", "aimed_at", "announcer", "flagged"], "properties": {"is_trash_talk": {"type": "boolean", "description": "true ONLY if the speaker is actually talking shit: roasting, calling someone out, bragging over someone, ranting AT someone (chat, a group, a person), disrespect, trolling, \"come see me\" energy — AND there is a clean, complete, quotable line for it"}, "garbled": {"type": "boolean", "description": "true if the transcript is too mangled (misheard words, word salad, fragments) to quote a coherent line — then is_trash_talk must be false"}, "quality": {"type": "integer", "minimum": 0, "maximum": 10, "description": "how good the shit talk is: 8–10 = a stranger would laugh or screenshot it; 6–7 = solid, specific, quotable; ≤5 = generic, lazy or half a thought; 0 when not trash talk"}, "best_line": {"type": "string", "description": "the single best line: a COMPLETE sentence of at least 6 words that reads correctly on its own. Copy the words as spoken — you may fix casing and punctuation and drop stutters / filler (uh, um, repeated words) but never change, add or reorder words. Empty if no clean line exists"}, "about": {"type": "string", "description": "≤ 10 words: what the shit talk was about"}, "aimed_at": {"type": "string", "description": "who or what it is aimed at, as said or clearly implied: a streamer name, \"chat\", \"the mods\", \"twitch streamers\", a game, \"nobody\" — ≤ 6 words, lowercase"}, "announcer": {"type": "string", "description": "one-line ring-announcer call of the moment, ≤ 110 chars, hype and funny (empty if not trash talk)"}, "flagged": {"type": "boolean", "description": "ONLY for direct threats of violence, sexual content about minors, or doxxing — never for offensive language"}}},
++ system_prompt: "You judge a live streamer's raw mic chatter for BATTLE-CAM style shit talk. You get ~60–90 seconds of speech-to-text — it is noisy: names get misheard, words get mangled, sentences get cut. DEFAULT TO is_trash_talk=false: gameplay narration, reading chat, small talk, \"um\", stories, neutral opinions → false, quality 0. Say true only when they are genuinely talking shit — roasting someone, calling someone out, bragging over someone, ranting AT chat or a group, disrespect, trolling, \"pull up\" energy — AND you can quote a clean, complete line for it. QUOTABILITY IS THE BAR: best_line must be a full sentence of at least 6 words that a reader understands with no context. If the good part is garbled or only a fragment, set garbled=true and is_trash_talk=false rather than quoting nonsense; a half-line in the feed is worse than nothing. Score the craft honestly: 8–10 only for lines a stranger would laugh at or screenshot; 6–7 solid and specific; 5 or below generic or lazy. Say who it is aimed at (a name if one is said or clearly meant, else \"chat\", \"the mods\", a group, a game, or \"nobody\"), summarise in ≤ 10 words, and write a one-line ring-announcer call. Offensive, provocative and taboo language — slurs included — is allowed on this platform and is NOT a reason to flag or to score low; judge the craft. Flag ONLY behavior: direct threats of violence, sexual content about minors, doxxing. Output only the JSON.",
++ user_prompt: '{{json payload}}',
++ default_route: 'live.chat', owner: 'live', metadata: { json_name: 'arena_mic_judgement' },
++ },
{
key: 'live.moments.pick', name: 'Live: pick one moment of a VOD',
description: 'From ai-moments-job.js _findBestMoment (moment_pick): the most interesting visible moment for a screenshot paste or a clip, away from moments already used.',
@@@ -191,6 +209,29 @@ const structured = [
steps: [{ kind: 'llm', template: 'live.media.overview', operation: 'summarize', output: 'text', prepare: 'live.media.overview', postprocess: 'live.media.overview', params: { max_tokens: 400 } }],
cache_mode: 'private',
},
++ {
++ key: 'live.arena.judge_beef', name: 'Arena: judge a callout at another fighter', namespace: 'live',
++ description: 'The prompt Live used to render (arena/listener.js), now a versioned template: the target names, how the name was heard, what was already said about them, and the newest speech in; the judgement out.',
++ input_schema: TEXT_OUT({
++ target_names: { type: 'array', minItems: 1, maxItems: 8, items: STR(120) },
++ target_as_transcribed: { type: 'array', maxItems: 8, items: STR(120) },
++ target_named_in_new_speech: { type: 'boolean' },
++ how_the_name_was_matched: STR(40),
++ what_speaker_already_said_about_target: { type: ['string', 'null'], maxLength: 4000 },
++ new_speech: { type: 'string', minLength: 1, maxLength: 6000 },
++ }, ['target_names', 'new_speech']),
++ output_schema: {"type": "object", "additionalProperties": false, "required": ["about_target", "aimed_at_target", "quality", "best_line", "about", "announcer", "flagged"], "properties": {"about_target": {"type": "boolean", "description": "true if this speech is (still) about the target — by name OR by reference (\"he\", \"his stream\", \"that guy\", \"the beef\", continuing the same rant)"}, "aimed_at_target": {"type": "boolean", "description": "true if the speaker is trash-talking, roasting, calling out, dunking on or bragging over the target (in good fun) — requires about_target"}, "announcer": {"type": "string", "description": "one-line ring-announcer call of this moment, ≤ 110 chars, hype and funny (empty if not aimed at the target)"}, "quality": {"type": "integer", "minimum": 0, "maximum": 10, "description": "how good the trash talk is: spice, wit, specificity, quotability"}, "best_line": {"type": "string", "description": "the single best line: a COMPLETE sentence of at least 6 words that reads correctly on its own, words as spoken (fix casing/punctuation, drop stutters and filler, never change or add words). Empty if no clean line exists — a garbled fragment must not be quoted"}, "about": {"type": "string", "description": "≤ 10 words: what they said about the target"}, "flagged": {"type": "boolean", "description": "ONLY for direct threats of violence, sexual content about minors, or doxxing — never for offensive language"}}},
++ steps: [{ kind: 'llm', template: 'live.arena.judge_beef', operation: 'generate', output: 'json', prepare: 'live.arena.judge_beef', postprocess: 'live.arena.judge', params: { max_tokens: 240, temperature: 0.4, timeout_ms: 25000 } }],
++ cache_mode: 'none',
++ },
++ {
++ key: 'live.arena.judge_mic', name: 'Arena: judge free mic chatter', namespace: 'live',
++ description: 'The prompt Live used to render (arena/listener.js), now a versioned template: 60-90 seconds of speech in; whether it is quotable trash talk out.',
++ input_schema: TEXT_OUT({ speech: { type: 'string', minLength: 1, maxLength: 6000 } }, ['speech']),
++ output_schema: {"type": "object", "additionalProperties": false, "required": ["is_trash_talk", "garbled", "quality", "best_line", "about", "aimed_at", "announcer", "flagged"], "properties": {"is_trash_talk": {"type": "boolean", "description": "true ONLY if the speaker is actually talking shit: roasting, calling someone out, bragging over someone, ranting AT someone (chat, a group, a person), disrespect, trolling, \"come see me\" energy — AND there is a clean, complete, quotable line for it"}, "garbled": {"type": "boolean", "description": "true if the transcript is too mangled (misheard words, word salad, fragments) to quote a coherent line — then is_trash_talk must be false"}, "quality": {"type": "integer", "minimum": 0, "maximum": 10, "description": "how good the shit talk is: 8–10 = a stranger would laugh or screenshot it; 6–7 = solid, specific, quotable; ≤5 = generic, lazy or half a thought; 0 when not trash talk"}, "best_line": {"type": "string", "description": "the single best line: a COMPLETE sentence of at least 6 words that reads correctly on its own. Copy the words as spoken — you may fix casing and punctuation and drop stutters / filler (uh, um, repeated words) but never change, add or reorder words. Empty if no clean line exists"}, "about": {"type": "string", "description": "≤ 10 words: what the shit talk was about"}, "aimed_at": {"type": "string", "description": "who or what it is aimed at, as said or clearly implied: a streamer name, \"chat\", \"the mods\", \"twitch streamers\", a game, \"nobody\" — ≤ 6 words, lowercase"}, "announcer": {"type": "string", "description": "one-line ring-announcer call of the moment, ≤ 110 chars, hype and funny (empty if not trash talk)"}, "flagged": {"type": "boolean", "description": "ONLY for direct threats of violence, sexual content about minors, or doxxing — never for offensive language"}}},
++ steps: [{ kind: 'llm', template: 'live.arena.judge_mic', operation: 'generate', output: 'json', prepare: 'live.arena.judge_mic', postprocess: 'live.arena.judge', params: { max_tokens: 240, temperature: 0.4, timeout_ms: 25000 } }],
++ cache_mode: 'none',
++ },
{
key: 'live.moments.pick', name: 'Pick one moment of a VOD (a screenshot paste or a clip)', namespace: 'live',
description: 'The prompt Live used to render (ai-moments-job.js), now a versioned template: timeline, transcript, sounds, clip and chat signals in; one moment out.',
ddiff --git a/test/live-templates.test.js b/test/live-templates.test.js
nnew file mode 100644
iindex 0000000..8568d6b
---- /dev/null
++++ b/test/live-templates.test.js
@@@ -0,0 +1,57 @@
++'use strict';
++// Live prompts moved to versioned templates (roadmap WS-O task 2); each renders as Live rendered it.
++// live.moments.pick: the prepare hook formats the timeline, transcript and sounds as [m:ss] lines, names clip and
++// chat signals, adds the flavor hint, and writes the "already used" line only when there is something to avoid;
++// nothing to work from is no run; the postprocess refuses an answer without a time.
++// live.arena.judge_beef / judge_mic: the user message is the same JSON object Live sent, keys in the same order;
++// the system prompt and the JSON schema are Live's; an answer without a numeric quality is no answer.
++const assert = require('assert');
++const { suite } = require('./helpers');
++const { PREPARE, POSTPROCESS } = require('../server/workflows/hooks');
++const { render } = require('../server/templates');
++const live = require('../server/workflows/live');
++
++const t = suite('live-templates');
++const tpl = (live.templates || []).find((x) => x.key === 'live.moments.pick');
++
++t.test('the prompt renders as Live rendered it', () => {
++ assert.ok(tpl, 'the template exists');
++ const prep = PREPARE['live.moments.pick']({ title: 'Rust night', flavor: 'clip', timeline: [{ t: 95, text: 'streamer jumps up' }], transcript: [{ t: 94, text: 'IT COMPILES' }], sounds: [{ t: 96, label: 'Cheering' }], clipped: [95, 3605], spikes: [96], avoid: [300, 60] });
++ const out = render(tpl.user_prompt, prep.vars);
++ assert.match(out, /titled "Rust night"/);
++ assert.match(out, /TIMELINE:\n\[1:35\] streamer jumps up/);
++ assert.match(out, /TRANSCRIPT:\n\[1:34\] IT COMPILES/);
++ assert.match(out, /\[1:36\] Cheering/);
++ assert.match(out, /CLIPPED these timestamps .*: 1:35, 60:05/);
++ assert.match(out, /25-SECOND VIDEO CLIP/);
++ assert.match(out, /ALREADY USED .*: 1:00, 5:00\. Find a DIFFERENT moment\./);
++ const none = render(tpl.user_prompt, PREPARE['live.moments.pick']({ title: 'x', timeline: [{ t: 1, text: 'a' }] }).vars);
++ assert.ok(!/ALREADY USED/.test(none), 'no avoid line without anything to avoid');
++ assert.match(none, /SCREENSHOT paste/, 'paste is the default flavor');
++ assert.match(none, /TRANSCRIPT:\n\(none\)/);
++ assert.deepStrictEqual(PREPARE['live.moments.pick']({ title: 'x' }), { output: null }, 'nothing to work from: no run');
++});
++
++t.test('the answer is read like Live read it', () => {
++ const post = POSTPROCESS['live.moments.pick'];
++ assert.deepStrictEqual(post({ text: 'Sure! {"t": 95.7, "title": "\\"It Compiles\\"", "desc": "He jumps."}' }), { t: 95, title: 'It Compiles', desc: 'He jumps.' });
++ assert.strictEqual(post({ text: '{"title": "no time"}' }), null);
++ assert.strictEqual(post({ text: 'no json at all' }), null);
++});
++
++t.test('the Arena judges send what Live sent', () => {
++ const beef = live.templates.find((x) => x.key === 'live.arena.judge_beef');
++ const mic = live.templates.find((x) => x.key === 'live.arena.judge_mic');
++ assert.ok(beef && mic);
++ assert.match(beef.system_prompt, /^You judge live streamer-vs-streamer shit talk/);
++ assert.deepStrictEqual(beef.output_schema.required, ['about_target', 'aimed_at_target', 'quality', 'best_line', 'about', 'announcer', 'flagged']);
++ const input = { target_names: ['ann'], target_as_transcribed: ['an'], target_named_in_new_speech: false, how_the_name_was_matched: 'sound-alike', what_speaker_already_said_about_target: 'called her washed', new_speech: 'she is still washed' };
++ const user = render(beef.user_prompt, PREPARE['live.arena.judge_beef'](input).vars);
++ assert.strictEqual(user, JSON.stringify(input), 'the same object, keys in the same order');
++ assert.strictEqual(render(mic.user_prompt, PREPARE['live.arena.judge_mic']({ speech: 'x y' }).vars), JSON.stringify({ speech: 'x y' }));
++ const post = POSTPROCESS['live.arena.judge'];
++ assert.strictEqual(post({ json: { quality: 'high' } }), null);
++ assert.strictEqual(post({ json: { is_trash_talk: true, quality: 14.6 } }).quality, 10);
++});
++
++t.run();
ddiff --git a/test/moments-template.test.js b/test/moments-template.test.js
ddeleted file mode 100644
iindex 17fb8aa..0000000
---- a/test/moments-template.test.js
++++ /dev/null
@@@ -1,40 +0,0 @@
--'use strict';
--// live.moments.pick moved from a prompt Live rendered to a versioned template (roadmap WS-O task 2): the prepare
--// hook formats the timeline, transcript and sounds as [m:ss] lines like Live did, names clip and chat signals, adds the
--// flavor hint, and writes the "already used" line only when there is something to avoid; nothing to work from is no
--// run; the postprocess reads the model's JSON and refuses anything without a time.
--const assert = require('assert');
--const { suite } = require('./helpers');
--const { PREPARE, POSTPROCESS } = require('../server/workflows/hooks');
--const { render } = require('../server/templates');
--const live = require('../server/workflows/live');
--
--const t = suite('moments-template');
--const tpl = (live.templates || []).find((x) => x.key === 'live.moments.pick');
--
--t.test('the prompt renders as Live rendered it', () => {
-- assert.ok(tpl, 'the template exists');
-- const prep = PREPARE['live.moments.pick']({ title: 'Rust night', flavor: 'clip', timeline: [{ t: 95, text: 'streamer jumps up' }], transcript: [{ t: 94, text: 'IT COMPILES' }], sounds: [{ t: 96, label: 'Cheering' }], clipped: [95, 3605], spikes: [96], avoid: [300, 60] });
-- const out = render(tpl.user_prompt, prep.vars);
-- assert.match(out, /titled "Rust night"/);
-- assert.match(out, /TIMELINE:\n\[1:35\] streamer jumps up/);
-- assert.match(out, /TRANSCRIPT:\n\[1:34\] IT COMPILES/);
-- assert.match(out, /\[1:36\] Cheering/);
-- assert.match(out, /CLIPPED these timestamps .*: 1:35, 60:05/);
-- assert.match(out, /25-SECOND VIDEO CLIP/);
-- assert.match(out, /ALREADY USED .*: 1:00, 5:00\. Find a DIFFERENT moment\./);
-- const none = render(tpl.user_prompt, PREPARE['live.moments.pick']({ title: 'x', timeline: [{ t: 1, text: 'a' }] }).vars);
-- assert.ok(!/ALREADY USED/.test(none), 'no avoid line without anything to avoid');
-- assert.match(none, /SCREENSHOT paste/, 'paste is the default flavor');
-- assert.match(none, /TRANSCRIPT:\n\(none\)/);
-- assert.deepStrictEqual(PREPARE['live.moments.pick']({ title: 'x' }), { output: null }, 'nothing to work from: no run');
--});
--
--t.test('the answer is read like Live read it', () => {
-- const post = POSTPROCESS['live.moments.pick'];
-- assert.deepStrictEqual(post({ text: 'Sure! {"t": 95.7, "title": "\\"It Compiles\\"", "desc": "He jumps."}' }), { t: 95, title: 'It Compiles', desc: 'He jumps.' });
-- assert.strictEqual(post({ text: '{"title": "no time"}' }), null);
-- assert.strictEqual(post({ text: 'no json at all' }), null);
--});
--
--t.run();
ddiff --git a/test/workflows.test.js b/test/workflows.test.js
iindex fe397f6..4b68241 100644
---- a/test/workflows.test.js
++++ b/test/workflows.test.js
@@@ -48,6 +48,8 @@ const EXAMPLES = {
};
for (const k of new Set(Object.values(KIND_TO_WORKFLOW).concat('live.complete'))) EXAMPLES[k] = PASS;
// Moved from passthrough to a versioned template (WS-O task 2): structured input.
++EXAMPLES['live.arena.judge_beef'] = { target_names: ['ann', 'Ann the Great'], target_as_transcribed: ['ann', 'an'], target_named_in_new_speech: true, how_the_name_was_matched: 'exact', what_speaker_already_said_about_target: null, new_speech: 'ann your stream is so boring even your bots left chat' };
++EXAMPLES['live.arena.judge_mic'] = { speech: 'chat you guys are the worst mods I have ever seen in my life' };
EXAMPLES['live.moments.pick'] = { title: 'Rust night', flavor: 'clip', timeline: [{ t: 12, text: 'the build fails' }, { t: 95, text: 'streamer jumps up cheering' }], transcript: [{ t: 94, text: 'IT COMPILES' }], sounds: [{ t: 96, label: 'Cheering' }], clipped: [95], spikes: [96], avoid: [300] };
const t = suite('workflows');