openvibers/openvibe.ai Commits

Live's Rundown workflows: live.rundown.segment (what a streamer talked about in a stretch of transcript: topic, headline, take, the desk's quip, verbatim quotes, game and video audio never theirs), live.rundown.episode (a stream as an episode: title, logline, recap, chapters) and live.rundown.topic (a topic's page across streamers), on the creative route (#122)

OP OpenVibers committed 25 days ago · b3225a2405 · 4 files, +140 −0

Changes

ddiff --git a/server/workflows/hooks.js b/server/workflows/hooks.js
iindex 391bd0b..3762c1c 100644
---- a/server/workflows/hooks.js
++++ b/server/workflows/hooks.js
@@@ -133,6 +133,22 @@ const PREPARE = {
'live.arena.drama'(input) {
return { vars: { payload: input } };
},
++ // The Rundown (Live rundown/desk.js): transcript lines as "[seconds] text", the known topics one per line.
++ 'live.rundown.segment'(input) {
++ const clean = (t, n) => String(t || '').replace(/\s+/g, ' ').trim().slice(0, n);
++ return { vars: {
++ streamer: clean(input.streamer, 80), title: clean(input.title, 200) || '(none)', category: clean(input.category, 80) || '(none)',
++ topics: (input.topics || []).slice(0, 60).map((t) => `- ${clean(t, 120)}`).join('\n') || '(none yet)',
++ earlier: clean(input.earlier, 800) || '(this is the start)',
++ lines: (input.lines || []).map((x) => `[${Math.round(Number(x.t) || 0)}] ${clean(x.text, 1000)}`).join('\n'),
++ } };
++ },
++ 'live.rundown.episode'(input) {
++ return { vars: { payload: input } };
++ },
++ 'live.rundown.topic'(input) {
++ return { vars: { payload: input } };
++ },
'live.clips.confirm'(input) {
const clean = (t, n) => String(t || '').replace(/\s+/g, ' ').trim().slice(0, n);
return { vars: {
@@@ -362,6 +378,42 @@ const POSTPROCESS = {
const alert = Math.min(5, Math.max(1, Math.round(Number(j.alert) || 1)));
return { headline: String(j.headline).replace(/\s+/g, ' ').trim().slice(0, 130), blurb: String(j.blurb).replace(/\s+/g, ' ').trim().slice(0, 480), alert };
},
++ // The Rundown: every field clamped; a segment without a topic, title or what is dropped; times stay inside the lines'.
++ 'live.rundown.segment'(r, input) {
++ const j = r.json || parseJsonLoose(r.text);
++ if (!j || !Array.isArray(j.segments)) return null;
++ const one = (t, n) => String(t == null ? '' : t).replace(/\s+/g, ' ').trim().slice(0, n);
++ const ts = (input && input.lines || []).map((x) => Number(x.t)).filter(Number.isFinite);
++ const lo = ts.length ? Math.min(...ts) : 0, hi = ts.length ? Math.max(...ts) : Infinity;
++ const at = (v) => { const n = Number(v); return Number.isFinite(n) ? Math.min(hi, Math.max(lo, n)) : lo; };
++ const KINDS = ['news', 'games', 'tech', 'life', 'music', 'sports', 'money', 'internet', 'chat', 'other'];
++ const segments = j.segments.filter((x) => x && one(x.topic, 120) && one(x.title, 90) && one(x.what, 500)).slice(0, 4).map((x) => {
++ const start = at(x.start), end = Math.max(start, at(x.end));
++ return {
++ start, end, topic: one(x.topic, 120), kind: KINDS.includes(x.kind) ? x.kind : 'other', title: one(x.title, 90), what: one(x.what, 500),
++ take: one(x.take, 300) || null, quip: one(x.quip, 220), score: Math.min(5, Math.max(1, Math.round(Number(x.score) || 1))),
++ quotes: (Array.isArray(x.quotes) ? x.quotes : []).filter((q) => q && one(q.text, 400)).slice(0, 2).map((q) => ({ t: at(q.t), text: one(q.text, 400), en: one(q.en, 400) || null })),
++ };
++ });
++ return { segments };
++ },
++ 'live.rundown.episode'(r, input) {
++ const j = r.json || parseJsonLoose(r.text);
++ if (!j || !j.title || !j.recap) return null;
++ const one = (t, n) => String(t == null ? '' : t).replace(/\s+/g, ' ').trim().slice(0, n);
++ const n = (input && input.segments || []).length;
++ const seen = new Set();
++ const chapters = (Array.isArray(j.chapters) ? j.chapters : []).filter((c) => c && Number.isInteger(c.index) && c.index >= 0 && c.index < n && !seen.has(c.index) && seen.add(c.index) && one(c.title, 60))
++ .slice(0, 10).map((c) => ({ index: c.index, title: one(c.title, 60) }));
++ const recap = String(j.recap).split(/\n\s*\n/).map((p) => p.replace(/\s+/g, ' ').trim()).filter(Boolean).slice(0, 4).join('\n\n').slice(0, 1200);
++ return { title: one(j.title, 80), logline: one(j.logline, 220), recap, chapters };
++ },
++ 'live.rundown.topic'(r) {
++ const j = r.json || parseJsonLoose(r.text);
++ if (!j || !j.summary) return null;
++ const one = (t, n) => String(t == null ? '' : t).replace(/\s+/g, ' ').trim().slice(0, n);
++ return { headline: one(j.headline, 100), summary: one(j.summary, 700) };
++ },
'live.clips.confirm'(r) {
const j = r.json || parseJsonLoose(r.text);
if (!j || typeof j !== 'object') return null;
ddiff --git a/server/workflows/live.js b/server/workflows/live.js
iindex 70985e4..fab59af 100644
---- a/server/workflows/live.js
++++ b/server/workflows/live.js
@@@ -151,6 +151,33 @@ const templates = [
user_prompt: "{{json payload}}",
default_route: 'live.creative', owner: 'live', metadata: { json_name: 'arena_drama' },
},
++ {
++ key: 'live.rundown.segment', name: "Live: what a streamer talked about (The Rundown)",
++ description: "From rundown/desk.js: a stretch of one stream's speech-to-text lines in; the segments of what the streamer talked about out (topic, headline, what, take, the desk's quip, verbatim quotes).",
++ input_schema: { type: 'object' },
++ output_schema: {"type": "object", "additionalProperties": false, "required": ["segments"], "properties": {"segments": {"type": "array", "maxItems": 4, "items": {"type": "object", "additionalProperties": false, "required": ["start", "end", "topic", "kind", "title", "what", "take", "quip", "score", "quotes"], "properties": {"start": {"type": "number"}, "end": {"type": "number"}, "topic": {"type": "string"}, "kind": {"type": "string", "enum": ["news", "games", "tech", "life", "music", "sports", "money", "internet", "chat", "other"]}, "title": {"type": "string"}, "what": {"type": "string"}, "take": {"type": ["string", "null"]}, "quip": {"type": "string"}, "score": {"type": "integer", "minimum": 1, "maximum": 5}, "quotes": {"type": "array", "maxItems": 2, "items": {"type": "object", "additionalProperties": false, "required": ["t", "text", "en"], "properties": {"t": {"type": "number"}, "text": {"type": "string"}, "en": {"type": ["string", "null"]}}}}}}}}},
++ system_prompt: "You are the writers' room of THE RUNDOWN, the part of a small live-streaming site that turns what streamers say on stream into stories people want to read. You get an automatic transcript of a stretch of one live stream and pull out what the streamer actually talked about. The transcript is speech recognition of the stream's whole audio: it mixes the streamer's own voice with game sounds, announcers and characters, videos and songs they play, articles they read aloud and other people, and it mishears words and sometimes the language. Rules: (1) Only the streamer's own talk is theirs: their opinions, stories, rants, jokes, plans, reactions and conversations with chat. A game's announcer or character lines (\"You have slain an enemy\", \"First blood\", \"Victory\"), lyrics, ads, a video's narration or an article read aloud are never the streamer; when they react to an article or a video, its subject is the topic and their reaction is the take. (2) Split the stretch into segments, one per thing they talked about, at most 4, in order. Skip stretches that are only gameplay noise, mumbling or garbled text; an empty list is a good answer when nothing was said. (3) topic: a short concrete name for the subject, the way a headline names it (\"Supermarket fridges hacked in France\", \"League ranked matchmaking\", \"Moving apartments\"), never vague (\"news\", \"gaming\", \"chatting\", \"the stream\"). When it is the same subject as one of the known topics, return that name exactly. (4) kind: news, games, tech, life, music, sports, money, internet, chat (talking with chat about the stream itself) or other. (5) title: a punchy headline for the segment, at most 70 characters. what: what they talked about, plainly, one or two sentences. take: their opinion or stance in one sentence, or null when they had none. quip: one line from the desk about the take or the situation, dry and a little savage, the way a late-night host would rib it, at most 160 characters. score: how much a reader would enjoy it, 1 (filler) to 5 (the best thing said all night). (6) quotes: 0 to 2 lines of the streamer's own words, copied character for character from the transcript lines (part of one line is fine), the funniest or most telling, each with its t. When a quote is not in English, en is an English translation; otherwise en is null. (7) start and end are seconds from the lines. Write topic, title, what, take and quip in English whatever language they speak. (8) Never invent anything that is not in what you are given: no events, numbers, motives, relationships or words. Never accuse anyone of a crime or of wronging someone; a streamer naming another streamer is them talking, not a fight. Talk about what they said and did, never who they are: no jokes about looks, body, clothing, age, race, religion, sexuality, gender or disability, no sexual content about anyone, no threats, no self-harm jokes. Swearing is fine. Write every name exactly as given, same spelling and capitals, and call people by their name or \"they\": never guess a pronoun (no he, him, his, she, her). Output only the JSON.",
++ user_prompt: "Streamer: {{streamer}}\nStream title: {{title}}\nCategory: {{category}}\nKnown topics (reuse the exact name for the same subject):\n{{topics}}\nEarlier in this stream: {{earlier}}\n\nTranscript lines ([seconds into the stream] text):\n{{lines}}",
++ default_route: 'live.creative', owner: 'live', metadata: { json_name: 'rundown_segment' },
++ },
++ {
++ key: 'live.rundown.episode', name: "Live: a stream written up as an episode (The Rundown)",
++ description: "From rundown/desk.js: a finished stream's facts and segments in; an episode title, logline, recap and chapters out.",
++ input_schema: { type: 'object' },
++ output_schema: {"type": "object", "additionalProperties": false, "required": ["title", "logline", "recap", "chapters"], "properties": {"title": {"type": "string"}, "logline": {"type": "string"}, "recap": {"type": "string"}, "chapters": {"type": "array", "maxItems": 10, "items": {"type": "object", "additionalProperties": false, "required": ["index", "title"], "properties": {"index": {"type": "integer", "minimum": 0}, "title": {"type": "string"}}}}}},
++ system_prompt: "You write the episode guide of THE RUNDOWN, where every live stream is an episode. You get the stream's facts and its segments, what the streamer talked about, in order. Write: title: the episode's title, at most 60 characters, playful and specific to this stream (never just the streamer's name or \"Stream\"). logline: one sentence of at most 180 characters, the hook that makes someone open it. recap: the story of the stream in order, two or three short paragraphs (blank line between them), at most 900 characters in all, with personality: dry, teasing, roasting the takes and the situations, never mean about the person. Quote only from the quotes given, word for word. chapters: up to 8 segments worth listing, each the segment's index and a short chapter title of at most 50 characters. Copy numbers exactly. Never invent anything that is not in what you are given: no events, numbers, motives, relationships or words. Never accuse anyone of a crime or of wronging someone; a streamer naming another streamer is them talking, not a fight. Talk about what they said and did, never who they are: no jokes about looks, body, clothing, age, race, religion, sexuality, gender or disability, no sexual content about anyone, no threats, no self-harm jokes. Swearing is fine. Write every name exactly as given, same spelling and capitals, and call people by their name or \"they\": never guess a pronoun (no he, him, his, she, her). Output only the JSON.",
++ user_prompt: "{{json payload}}",
++ default_route: 'live.creative', owner: 'live', metadata: { json_name: 'rundown_episode' },
++ },
++ {
++ key: 'live.rundown.topic', name: "Live: a topic's page across streamers (The Rundown)",
++ description: "From rundown/desk.js: one topic and every take on it in; a headline and a summary out.",
++ input_schema: { type: 'object' },
++ output_schema: {"type": "object", "additionalProperties": false, "required": ["headline", "summary"], "properties": {"headline": {"type": "string"}, "summary": {"type": "string"}}},
++ system_prompt: "You run THE RUNDOWN's topic pages. You get one topic and the takes streamers had on it, oldest first. Write: headline: at most 80 characters, punchy, about the topic and the takes. summary: two to four sentences, at most 600 characters: what the subject is, who said what, where they agree or clash, and one dry line from the desk. Quote only from the quotes given, word for word. Never invent anything that is not in what you are given: no events, numbers, motives, relationships or words. Never accuse anyone of a crime or of wronging someone; a streamer naming another streamer is them talking, not a fight. Talk about what they said and did, never who they are: no jokes about looks, body, clothing, age, race, religion, sexuality, gender or disability, no sexual content about anyone, no threats, no self-harm jokes. Swearing is fine. Write every name exactly as given, same spelling and capitals, and call people by their name or \"they\": never guess a pronoun (no he, him, his, she, her). Output only the JSON.",
++ user_prompt: "{{json payload}}",
++ default_route: 'live.creative', owner: 'live', metadata: { json_name: 'rundown_topic' },
++ },
{
key: 'live.clips.confirm', name: "Live: is this chat spike clip-worthy",
description: "From auto-clip-job.js (auto_clip_confirm): scene notes, transcript, sounds and chat around a spike.",
@@@ -426,6 +453,30 @@ const structured = [
steps: [{ kind: 'llm', template: 'live.arena.drama', operation: 'generate', output: 'json', prepare: 'live.arena.drama', postprocess: 'live.arena.drama', params: { max_tokens: 220, temperature: 1.0, timeout_ms: 20000 } }],
cache_mode: 'none',
},
++ {
++ key: 'live.rundown.segment', name: "The Rundown: what a streamer talked about", namespace: 'live',
++ description: "rundown/desk.js: a stretch of a stream's transcript lines (with the stream's title and category, the known topics and what came earlier) in; up to 4 segments out, each with a topic, headline, what, take, quip, score and verbatim quotes.",
++ input_schema: TEXT_OUT({ streamer: STR(80), title: STR(200), category: STR(80), topics: { type: 'array', maxItems: 60, items: STR(120) }, earlier: STR(800), lines: { type: 'array', minItems: 1, maxItems: 400, items: { type: 'object', required: ['t', 'text'], additionalProperties: false, properties: { t: { type: 'number', minimum: 0 }, text: STR(1000) } } } }, ['streamer', 'lines']),
++ output_schema: {"type": "object", "additionalProperties": false, "required": ["segments"], "properties": {"segments": {"type": "array", "maxItems": 4, "items": {"type": "object", "additionalProperties": false, "required": ["start", "end", "topic", "kind", "title", "what", "take", "quip", "score", "quotes"], "properties": {"start": {"type": "number"}, "end": {"type": "number"}, "topic": {"type": "string"}, "kind": {"type": "string", "enum": ["news", "games", "tech", "life", "music", "sports", "money", "internet", "chat", "other"]}, "title": {"type": "string"}, "what": {"type": "string"}, "take": {"type": ["string", "null"]}, "quip": {"type": "string"}, "score": {"type": "integer", "minimum": 1, "maximum": 5}, "quotes": {"type": "array", "maxItems": 2, "items": {"type": "object", "additionalProperties": false, "required": ["t", "text", "en"], "properties": {"t": {"type": "number"}, "text": {"type": "string"}, "en": {"type": ["string", "null"]}}}}}}}}},
++ steps: [{ kind: 'llm', template: 'live.rundown.segment', operation: 'generate', output: 'json', prepare: 'live.rundown.segment', postprocess: 'live.rundown.segment', params: { max_tokens: 1400, temperature: 1.0, timeout_ms: 60000 } }],
++ cache_mode: 'none',
++ },
++ {
++ key: 'live.rundown.episode', name: "The Rundown: a stream as an episode", namespace: 'live',
++ description: "rundown/desk.js: a finished stream's facts and its segments in; an episode title, logline, recap and chapters out.",
++ input_schema: TEXT_OUT({ streamer: STR(80), title: STR(200), category: STR(80), minutes: { type: 'integer', minimum: 0 }, peak_viewers: { type: ['integer', 'null'], minimum: 0 }, chat_messages: { type: ['integer', 'null'], minimum: 0 }, segments: { type: 'array', minItems: 1, maxItems: 40, items: { type: 'object', required: ['t', 'title', 'what'], additionalProperties: false, properties: { t: { type: 'number', minimum: 0 }, topic: STR(120), title: STR(120), what: STR(500), take: { type: ['string', 'null'], maxLength: 300 }, quotes: { type: 'array', maxItems: 2, items: STR(400) } } } } }, ['streamer', 'segments']),
++ output_schema: {"type": "object", "additionalProperties": false, "required": ["title", "logline", "recap", "chapters"], "properties": {"title": {"type": "string"}, "logline": {"type": "string"}, "recap": {"type": "string"}, "chapters": {"type": "array", "maxItems": 10, "items": {"type": "object", "additionalProperties": false, "required": ["index", "title"], "properties": {"index": {"type": "integer", "minimum": 0}, "title": {"type": "string"}}}}}},
++ steps: [{ kind: 'llm', template: 'live.rundown.episode', operation: 'generate', output: 'json', prepare: 'live.rundown.episode', postprocess: 'live.rundown.episode', params: { max_tokens: 900, temperature: 1.0, timeout_ms: 60000 } }],
++ cache_mode: 'none',
++ },
++ {
++ key: 'live.rundown.topic', name: "The Rundown: a topic's page", namespace: 'live',
++ description: "rundown/desk.js: a topic and every take on it in; a headline and summary out.",
++ input_schema: TEXT_OUT({ topic: STR(120), takes: { type: 'array', minItems: 1, maxItems: 30, items: { type: 'object', required: ['streamer', 'what'], additionalProperties: false, properties: { streamer: STR(80), when: STR(40), what: STR(500), take: { type: ['string', 'null'], maxLength: 300 }, quote: { type: ['string', 'null'], maxLength: 400 } } } } }, ['topic', 'takes']),
++ output_schema: {"type": "object", "additionalProperties": false, "required": ["headline", "summary"], "properties": {"headline": {"type": "string"}, "summary": {"type": "string"}}},
++ steps: [{ kind: 'llm', template: 'live.rundown.topic', operation: 'generate', output: 'json', prepare: 'live.rundown.topic', postprocess: 'live.rundown.topic', params: { max_tokens: 400, temperature: 1.0, timeout_ms: 30000 } }],
++ cache_mode: 'none',
++ },
{
key: 'live.clips.confirm', name: "Is this chat spike clip-worthy", namespace: 'live',
description: "The prompt Live used to render (auto-clip-job.js): scene notes, timed speech and sounds, and chat around a spike in; clip or not, with a title, out.",
ddiff --git a/test/live-templates.test.js b/test/live-templates.test.js
iindex 45cedfa..dff26ea 100644
---- a/test/live-templates.test.js
++++ b/test/live-templates.test.js
@@@ -157,4 +157,38 @@ t.test("a channel description is public prose: never about looks, no lists or la
assert.deepStrictEqual(POSTPROCESS['live.media.overview']({ text: 'They build a PC.\nThen they test it' }), { overview: 'They build a PC. Then they test it.' });
});
++t.test('The Rundown: the segment prompt lists the lines and known topics; its output is clamped, kept to the lines and stripped of empty segments', () => {
++ const tp = (k) => live.templates.find((x) => x.key === k);
++ const seg = tp('live.rundown.segment');
++ assert.strictEqual(seg.default_route, 'live.creative');
++ assert.match(seg.system_prompt, /announcer or character lines/);
++ assert.match(seg.system_prompt, /never guess a pronoun/);
++ const input = { streamer: 'goosely', title: 'News', category: 'Tech', topics: ['Supermarket fridges hacked in France'], lines: [{ t: 1081, text: 'The person that deals with refrigeration, I\'m not going to say is stupid.' }, { t: 1096.6, text: 'This is why having the internet inside of a refrigerator is a bad idea.' }] };
++ const user = render(seg.user_prompt, PREPARE['live.rundown.segment'](input).vars);
++ assert.ok(user.includes('Streamer: goosely') && user.includes('- Supermarket fridges hacked in France') && user.includes('[1097] This is why having the internet'), user);
++ assert.ok(user.includes('Earlier in this stream: (this is the start)'));
++ const out = POSTPROCESS['live.rundown.segment']({ json: { segments: [
++ { start: 900, end: 5000, topic: 'Supermarket fridges hacked in France', kind: 'tech', title: 'Fridges on the internet', what: 'They read about fridges.', take: 'Internet fridges are a bad idea.', quip: 'Cold take.', score: 9, quotes: [{ t: 1096.6, text: 'a bad idea', en: '' }, { t: 1, text: '' }] },
++ { start: 1000, end: 1001, topic: '', title: 'x', what: 'y' },
++ { start: 1000, end: 1001, topic: 'T', title: 'x', what: 'y', kind: 'weird' },
++ ] } }, input);
++ assert.strictEqual(out.segments.length, 2, 'a segment with no topic is dropped');
++ assert.deepStrictEqual([out.segments[0].start, out.segments[0].end, out.segments[0].score], [1081, 1096.6, 5], 'times inside the lines, score 1-5');
++ assert.deepStrictEqual(out.segments[0].quotes, [{ t: 1096.6, text: 'a bad idea', en: null }]);
++ assert.strictEqual(out.segments[1].kind, 'other');
++ assert.strictEqual(POSTPROCESS['live.rundown.segment']({ text: 'nope' }, input), null);
++ assert.deepStrictEqual(POSTPROCESS['live.rundown.segment']({ json: { segments: [] } }, input), { segments: [] }, 'nothing said is an answer');
++});
++
++t.test('The Rundown: an episode keeps chapters that name a real segment once; a topic is clamped', () => {
++ const ep = POSTPROCESS['live.rundown.episode']({ json: { title: 'The Fridge Files', logline: 'A night of cold takes.', recap: 'One.\n\nTwo.\n\n\nThree.', chapters: [{ index: 0, title: 'Fridges' }, { index: 0, title: 'Again' }, { index: 7, title: 'Nope' }, { index: 1, title: '' }] } }, { segments: [{}, {}] });
++ assert.deepStrictEqual(ep, { title: 'The Fridge Files', logline: 'A night of cold takes.', recap: 'One.\n\nTwo.\n\nThree.', chapters: [{ index: 0, title: 'Fridges' }] });
++ assert.strictEqual(POSTPROCESS['live.rundown.episode']({ json: { title: 'x' } }, { segments: [] }), null, 'no recap, no episode');
++ assert.deepStrictEqual(POSTPROCESS['live.rundown.topic']({ json: { headline: ' Fridges ', summary: 'Two streamers agree.' } }), { headline: 'Fridges', summary: 'Two streamers agree.' });
++ const tp = (k) => live.templates.find((x) => x.key === k);
++ const payload = { topic: 'T', takes: [{ streamer: 'a', what: 'b' }] };
++ assert.strictEqual(render(tp('live.rundown.topic').user_prompt, PREPARE['live.rundown.topic'](payload).vars), JSON.stringify(payload));
++ for (const k of ['live.rundown.segment', 'live.rundown.episode', 'live.rundown.topic']) assert.ok(live.workflows.find((w) => w.key === k && w.steps[0].template === k), k);
++});
++
t.run();
ddiff --git a/test/workflows.test.js b/test/workflows.test.js
iindex c974f43..5c44eaf 100644
---- a/test/workflows.test.js
++++ b/test/workflows.test.js
@@@ -66,6 +66,9 @@ EXAMPLES['live.arena.persona'] = { facts: { name: 'Ann', handle: 'ann', category
EXAMPLES['live.arena.quotes'] = { lines: ['this borrow checker hates me and I hate it back', 'chat you are all backseat compilers', 'I have never been this cooked'] };
EXAMPLES['live.arena.headline'] = { kind: 'open', a: 'ann', b: 'bob', line: 'bob streams to his own alt accounts', rematch: false };
EXAMPLES['live.arena.drama'] = { kind: 'rage_quit', streamer: 'bob', other: null, what: 'bob usually goes 140 minutes. Today it lasted 6.', facts: { minutes_live: 6, usual_minutes: 140, last_title: 'ranked grind' }, quote: 'this game is rigged' };
++EXAMPLES['live.rundown.segment'] = { streamer: 'goosely', title: 'Tech news', category: 'Science & Technology', topics: ['Supermarket fridges hacked in France'], earlier: 'Opened with a router rant.', lines: [{ t: 1081, text: 'The person that deals with refrigeration, I am not going to say is stupid.' }, { t: 1096, text: 'Having the internet inside of a refrigerator is a bad idea.' }] };
++EXAMPLES['live.rundown.episode'] = { streamer: 'goosely', title: 'Tech news', category: 'Science & Technology', minutes: 140, peak_viewers: 6, chat_messages: 210, segments: [{ t: 1081, topic: 'Supermarket fridges hacked in France', title: 'Fridges on the internet', what: 'They read about hacked supermarket fridges.', take: 'Internet fridges are a bad idea.', quotes: ['Having the internet inside of a refrigerator is a bad idea.'] }] };
++EXAMPLES['live.rundown.topic'] = { topic: 'Supermarket fridges hacked in France', takes: [{ streamer: 'goosely', when: '2026-10-18', what: 'They read about hacked supermarket fridges.', take: 'Internet fridges are a bad idea.', quote: 'Having the internet inside of a refrigerator is a bad idea.' }] };
EXAMPLES['live.clips.confirm'] = { scene: ['streamer jumps up cheering'], transcript: [{ t: 94, text: 'IT COMPILES' }], sounds: [{ t: 95, label: 'Cheering', confidence: 0.8 }], chat: ['LETS GO', 'poggers'] };
EXAMPLES['live.chat.global'] = { window_label: 'past 3 hours', prior_memory: 'croutons are a running joke', recent_labels: ['the crouton war'], messages: [{ mins_ago: 12, where: 'global', author: 'goosely', kind: null, text: 'croutons are soup confetti' }, { mins_ago: 3, where: '#ann', author: 'bob', kind: 'emote', text: 'waves' }] };
EXAMPLES['live.chat.profile'] = { subject_kind: 'relay', name: 'KickFan', platform: 'kick', recent_24h: false, seen: 0, prior_memory: '', messages: [{ mins_ago: 2000, author: 'KickFan', text: 'hello from kick' }] };