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.
Changes
@@@ -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);@@@ -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.',nnew file mode 100644@@@ -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();ddeleted file mode 100644@@@ -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();@@@ -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');
@@ -76,6 +76,16 @@ const MOMENT_FLAVOR = {
@@ -76,6 +76,16 @@ const MOMENT_FLAVOR = {
const oneLine = (t, max) => String(t || '').replace(/\s+/g, ' ').trim().slice(0, max);
const oneLine = (t, max) => String(t || '').replace(/\s+/g, ' ').trim().slice(0, max);
const PREPARE = {
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) {
'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 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');
const times = (list, n) => ((list || []).length ? list.slice(0, n).map((t) => mmss(Number(t) || 0)).join(', ') : 'none');
@@ -188,6 +198,12 @@ const PREPARE = {
@@ -188,6 +198,12 @@ const PREPARE = {
};
};
const POSTPROCESS = {
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) {
'live.moments.pick'(r) {
const j = r.json || parseJsonLoose(r.text);
const j = r.json || parseJsonLoose(r.text);
const t = j && Number(j.t);
const t = j && Number(j.t);
@@ -79,6 +79,24 @@ const templates = [
@@ -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}}',
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',
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',
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.',
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 = [
@@ -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 } }],
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',
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',
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.',
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.',
new file mode 100644
new file mode 100644
@@ -0,0 +1,57 @@
@@ -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();
deleted file mode 100644
deleted file mode 100644
@@ -1,40 +0,0 @@
@@ -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();
@@ -48,6 +48,8 @@ const EXAMPLES = {
@@ -48,6 +48,8 @@ const EXAMPLES = {
};
};
for (const k of new Set(Object.values(KIND_TO_WORKFLOW).concat('live.complete'))) EXAMPLES[k] = PASS;
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.
// 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] };
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');
const t = suite('workflows');