openvibers/openvibe.live Commits
The Rundown keeps segments scored 3 or more (a 1-2 is filler: coffee breaks, garbled stretches) and its front page shows takes scored 4 or more, crossovers and episodes (#258)
Changes
@@@ -19,7 +19,7 @@- a **topic**, concrete ("Supermarket fridges hacked in France", never "news"), and its kind (news, games, tech, life, music, sports, money, internet, chat, other);- a **headline**, **what** they talked about and **their take**;- the desk's **quip**, a dry late-night-host line about the take or the situation;-- - a 1–5 **score**;++ - a 1–5 **score** (a 1 or 2 is filler and isn't kept);- up to two **quotes**, copied word for word, with an English translation when they spoke another language.3. **Topics.** A segment joins the topic with the same slug, else a recent topic whose key words mostly match, else a new topic. A topic with new takes (2 or more in all) gets its page rewritten by `live.rundown.topic`: a headline, plus a summary of who said what and where they agree or clash.4. **Episodes.** When an ended stream is read to the end, `live.rundown.episode` writes it up from its segments: title, logline, a recap in 2–3 short paragraphs and chapters (VOD timestamps). A stream with no segments gets no episode.@@@ -46,7 +46,7 @@ Speech-to-text hears a stream's whole audio track. On 2026-10-18 the Arena's "qu- `/rundown`, the front page:- the masthead (date, counts) and the "On air now" strip, showing who is live and what they are talking about;-- - **The latest:** episodes and the best takes (score 3 and up, or naming someone), newest first;++ - **The latest:** episodes and the best takes (score 4 and up, or naming someone), newest first;- **Trending topics** this week (most streamers, then most takes);- **Who's been talking:** each streamer with their top topics, and how the desk works.- `/rundown/topics` lists every topic, by most recent or most talked about, with a filter by kind.@@@ -34,6 +34,7 @@ const BACKFILL_DAYS = 7;const CALLS_PER_TICK = 4;const CALLS_PER_HOUR = 40;const KNOWN_TOPICS = 50;++const MIN_SCORE = 3; // a 1-2 is filler ("heading out for coffee", a garbled stretch): not keptconst ANNOUNCE_PER_HOUR = 3;const ANNOUNCE_WITHIN_H = 6;@@@ -217,7 +218,7 @@ async function keepSegments(stream, stretch, segments, user, others = []) {for (const s of segments || []) {const title = clean(s.title, 90), what = clean(s.what, 500), take = clean(s.take, 300);let quip = clean(s.quip, 220);-- if (!title || !what || !s.topic) continue;++ if (!title || !what || !s.topic || (Number(s.score) || 1) < MIN_SCORE) continue;// An accusation the transcript never made is the desk's invention: the segment goes (its quip alone, the quip).if (filters.addsAccusation(`${title} ${what} ${take || ''}`, source)) continue;// A detail that places someone (a time zone, a school, an address…) drops the segment; in a quip, the quip.@@@ -13,7 +13,7 @@const db = require('../db/database');const REACTIONS = ['fire', 'lol', 'dead', 'cap', 'facts'];--const FEED_MIN_SCORE = 3;++const FEED_MIN_SCORE = 4; // the front page: a take scored 4 or 5, a crossover or an episodeconst parseJson = (s, d) => { try { return JSON.parse(s); } catch { return d; } };const daysAgo = (n) => new Date(Date.now() - n * 86400000).toISOString().slice(0, 19).replace('T', ' ');@@@ -75,7 +75,8 @@ const answers = {{ start: 0, end: 40, topic: 'Supermarket fridges hacked in France', kind: 'tech', title: 'French fridges get hacked', what: 'goosely read about supermarket fridges in France getting hacked.', take: 'Putting the internet in a fridge is a bad idea.',quip: 'Somewhere a fridge is reading this.', score: 4, quotes: [{ t: 20, text: 'having the internet inside of a refrigerator is a bad idea', en: null }, { t: 20, text: 'fridges deserve rights', en: null }, { t: 10, text: 'You have slain an enemy', en: null }] },{ start: 40, end: 60, topic: 'Fridge security', kind: 'tech', title: 'goosely threatens fridge makers', what: 'An invented threat.', take: null, quip: 'x', score: 3, quotes: [] },-- { start: 60, end: 240, topic: 'League ranked matchmaking', kind: 'games', title: 'Ranked is pain', what: 'goosely talked about ranked.', take: null, quip: 'goosely attacked the ladder.', score: 2, quotes: [] },++ { start: 60, end: 240, topic: 'League ranked matchmaking', kind: 'games', title: 'Ranked is pain', what: 'goosely talked about ranked.', take: null, quip: 'goosely attacked the ladder.', score: 3, quotes: [] },++ { start: 240, end: 250, topic: 'Coffee', kind: 'life', title: 'Filler bit', what: 'goosely got a coffee.', take: null, quip: 'x', score: 2, quotes: [] },] : [{ start: 0, end: 100, topic: 'Supermarket fridges hacked in France', kind: 'tech', title: 'iZoR_One weighs in on the fridge hack', what: 'iZoR_One also talked about the hacked fridges.', take: 'Fridges should stay dumb.', quip: 'A rare unity.', score: 3, quotes: [] },] }),@@@ -137,7 +138,8 @@ console.log('✅ desk: live streams wait for talk; topics gather streamers and gconst home = await read.home({ viewer: 'anon:t' });assert.ok(home.latest.some((x) => x.type === 'episode' && x.title === 'The Fridge Files'), 'the episode is in the latest');assert.ok(home.latest.some((x) => x.type === 'segment' && x.title === 'French fridges get hacked'), 'a score-4 take is in the latest');--assert.ok(!home.latest.some((x) => x.title === 'Ranked is pain'), 'a score-2 take without a name is not on the front page');++assert.ok(!await db.get("SELECT 1 FROM rundown_segments WHERE title = 'Filler bit'"), 'a score-2 segment is filler: not kept');++assert.ok(!home.latest.some((x) => x.title === 'Ranked is pain'), 'a score-3 take without a name is not on the front page');assert.strictEqual(home.trending[0].slug, 'supermarket-fridges-hacked-in-france');assert.strictEqual(home.trending[0].streamers, 2);assert.deepStrictEqual(Object.keys(home.latest[0].user).sort(), ['avatar_url', 'display_name', 'id', 'profile_color', 'username'], 'a person is five fields, never a users row');
@@ -19,7 +19,7 @@
@@ -19,7 +19,7 @@
- a **topic**, concrete ("Supermarket fridges hacked in France", never "news"), and its kind (news, games, tech, life, music, sports, money, internet, chat, other);
- a **topic**, concrete ("Supermarket fridges hacked in France", never "news"), and its kind (news, games, tech, life, music, sports, money, internet, chat, other);
- a **headline**, **what** they talked about and **their take**;
- a **headline**, **what** they talked about and **their take**;
- the desk's **quip**, a dry late-night-host line about the take or the situation;
- the desk's **quip**, a dry late-night-host line about the take or the situation;
- - a 1–5 **score**;
+ - a 1–5 **score** (a 1 or 2 is filler and isn't kept);
- up to two **quotes**, copied word for word, with an English translation when they spoke another language.
- up to two **quotes**, copied word for word, with an English translation when they spoke another language.
3. **Topics.** A segment joins the topic with the same slug, else a recent topic whose key words mostly match, else a new topic. A topic with new takes (2 or more in all) gets its page rewritten by `live.rundown.topic`: a headline, plus a summary of who said what and where they agree or clash.
3. **Topics.** A segment joins the topic with the same slug, else a recent topic whose key words mostly match, else a new topic. A topic with new takes (2 or more in all) gets its page rewritten by `live.rundown.topic`: a headline, plus a summary of who said what and where they agree or clash.
4. **Episodes.** When an ended stream is read to the end, `live.rundown.episode` writes it up from its segments: title, logline, a recap in 2–3 short paragraphs and chapters (VOD timestamps). A stream with no segments gets no episode.
4. **Episodes.** When an ended stream is read to the end, `live.rundown.episode` writes it up from its segments: title, logline, a recap in 2–3 short paragraphs and chapters (VOD timestamps). A stream with no segments gets no episode.
@@ -46,7 +46,7 @@ Speech-to-text hears a stream's whole audio track. On 2026-10-18 the Arena's "qu
@@ -46,7 +46,7 @@ Speech-to-text hears a stream's whole audio track. On 2026-10-18 the Arena's "qu
- `/rundown`, the front page:
- `/rundown`, the front page:
- the masthead (date, counts) and the "On air now" strip, showing who is live and what they are talking about;
- the masthead (date, counts) and the "On air now" strip, showing who is live and what they are talking about;
- - **The latest:** episodes and the best takes (score 3 and up, or naming someone), newest first;
+ - **The latest:** episodes and the best takes (score 4 and up, or naming someone), newest first;
- **Trending topics** this week (most streamers, then most takes);
- **Trending topics** this week (most streamers, then most takes);
- **Who's been talking:** each streamer with their top topics, and how the desk works.
- **Who's been talking:** each streamer with their top topics, and how the desk works.
- `/rundown/topics` lists every topic, by most recent or most talked about, with a filter by kind.
- `/rundown/topics` lists every topic, by most recent or most talked about, with a filter by kind.
@@ -34,6 +34,7 @@ const BACKFILL_DAYS = 7;
@@ -34,6 +34,7 @@ const BACKFILL_DAYS = 7;
const CALLS_PER_TICK = 4;
const CALLS_PER_TICK = 4;
const CALLS_PER_HOUR = 40;
const CALLS_PER_HOUR = 40;
const KNOWN_TOPICS = 50;
const KNOWN_TOPICS = 50;
+const MIN_SCORE = 3; // a 1-2 is filler ("heading out for coffee", a garbled stretch): not kept
const ANNOUNCE_PER_HOUR = 3;
const ANNOUNCE_PER_HOUR = 3;
const ANNOUNCE_WITHIN_H = 6;
const ANNOUNCE_WITHIN_H = 6;
@@ -217,7 +218,7 @@ async function keepSegments(stream, stretch, segments, user, others = []) {
@@ -217,7 +218,7 @@ async function keepSegments(stream, stretch, segments, user, others = []) {
for (const s of segments || []) {
for (const s of segments || []) {
const title = clean(s.title, 90), what = clean(s.what, 500), take = clean(s.take, 300);
const title = clean(s.title, 90), what = clean(s.what, 500), take = clean(s.take, 300);
let quip = clean(s.quip, 220);
let quip = clean(s.quip, 220);
- if (!title || !what || !s.topic) continue;
+ if (!title || !what || !s.topic || (Number(s.score) || 1) < MIN_SCORE) continue;
// An accusation the transcript never made is the desk's invention: the segment goes (its quip alone, the quip).
// An accusation the transcript never made is the desk's invention: the segment goes (its quip alone, the quip).
if (filters.addsAccusation(`${title} ${what} ${take || ''}`, source)) continue;
if (filters.addsAccusation(`${title} ${what} ${take || ''}`, source)) continue;
// A detail that places someone (a time zone, a school, an address…) drops the segment; in a quip, the quip.
// A detail that places someone (a time zone, a school, an address…) drops the segment; in a quip, the quip.
@@ -13,7 +13,7 @@
@@ -13,7 +13,7 @@
const db = require('../db/database');
const db = require('../db/database');
const REACTIONS = ['fire', 'lol', 'dead', 'cap', 'facts'];
const REACTIONS = ['fire', 'lol', 'dead', 'cap', 'facts'];
-const FEED_MIN_SCORE = 3;
+const FEED_MIN_SCORE = 4; // the front page: a take scored 4 or 5, a crossover or an episode
const parseJson = (s, d) => { try { return JSON.parse(s); } catch { return d; } };
const parseJson = (s, d) => { try { return JSON.parse(s); } catch { return d; } };
const daysAgo = (n) => new Date(Date.now() - n * 86400000).toISOString().slice(0, 19).replace('T', ' ');
const daysAgo = (n) => new Date(Date.now() - n * 86400000).toISOString().slice(0, 19).replace('T', ' ');
@@ -75,7 +75,8 @@ const answers = {
@@ -75,7 +75,8 @@ const answers = {
{ start: 0, end: 40, topic: 'Supermarket fridges hacked in France', kind: 'tech', title: 'French fridges get hacked', what: 'goosely read about supermarket fridges in France getting hacked.', take: 'Putting the internet in a fridge is a bad idea.',
{ start: 0, end: 40, topic: 'Supermarket fridges hacked in France', kind: 'tech', title: 'French fridges get hacked', what: 'goosely read about supermarket fridges in France getting hacked.', take: 'Putting the internet in a fridge is a bad idea.',
quip: 'Somewhere a fridge is reading this.', score: 4, quotes: [{ t: 20, text: 'having the internet inside of a refrigerator is a bad idea', en: null }, { t: 20, text: 'fridges deserve rights', en: null }, { t: 10, text: 'You have slain an enemy', en: null }] },
quip: 'Somewhere a fridge is reading this.', score: 4, quotes: [{ t: 20, text: 'having the internet inside of a refrigerator is a bad idea', en: null }, { t: 20, text: 'fridges deserve rights', en: null }, { t: 10, text: 'You have slain an enemy', en: null }] },
{ start: 40, end: 60, topic: 'Fridge security', kind: 'tech', title: 'goosely threatens fridge makers', what: 'An invented threat.', take: null, quip: 'x', score: 3, quotes: [] },
{ start: 40, end: 60, topic: 'Fridge security', kind: 'tech', title: 'goosely threatens fridge makers', what: 'An invented threat.', take: null, quip: 'x', score: 3, quotes: [] },
- { start: 60, end: 240, topic: 'League ranked matchmaking', kind: 'games', title: 'Ranked is pain', what: 'goosely talked about ranked.', take: null, quip: 'goosely attacked the ladder.', score: 2, quotes: [] },
+ { start: 60, end: 240, topic: 'League ranked matchmaking', kind: 'games', title: 'Ranked is pain', what: 'goosely talked about ranked.', take: null, quip: 'goosely attacked the ladder.', score: 3, quotes: [] },
+ { start: 240, end: 250, topic: 'Coffee', kind: 'life', title: 'Filler bit', what: 'goosely got a coffee.', take: null, quip: 'x', score: 2, quotes: [] },
] : [
] : [
{ start: 0, end: 100, topic: 'Supermarket fridges hacked in France', kind: 'tech', title: 'iZoR_One weighs in on the fridge hack', what: 'iZoR_One also talked about the hacked fridges.', take: 'Fridges should stay dumb.', quip: 'A rare unity.', score: 3, quotes: [] },
{ start: 0, end: 100, topic: 'Supermarket fridges hacked in France', kind: 'tech', title: 'iZoR_One weighs in on the fridge hack', what: 'iZoR_One also talked about the hacked fridges.', take: 'Fridges should stay dumb.', quip: 'A rare unity.', score: 3, quotes: [] },
] }),
] }),
@@ -137,7 +138,8 @@ console.log('✅ desk: live streams wait for talk; topics gather streamers and g
@@ -137,7 +138,8 @@ console.log('✅ desk: live streams wait for talk; topics gather streamers and g
const home = await read.home({ viewer: 'anon:t' });
const home = await read.home({ viewer: 'anon:t' });
assert.ok(home.latest.some((x) => x.type === 'episode' && x.title === 'The Fridge Files'), 'the episode is in the latest');
assert.ok(home.latest.some((x) => x.type === 'episode' && x.title === 'The Fridge Files'), 'the episode is in the latest');
assert.ok(home.latest.some((x) => x.type === 'segment' && x.title === 'French fridges get hacked'), 'a score-4 take is in the latest');
assert.ok(home.latest.some((x) => x.type === 'segment' && x.title === 'French fridges get hacked'), 'a score-4 take is in the latest');
-assert.ok(!home.latest.some((x) => x.title === 'Ranked is pain'), 'a score-2 take without a name is not on the front page');
+assert.ok(!await db.get("SELECT 1 FROM rundown_segments WHERE title = 'Filler bit'"), 'a score-2 segment is filler: not kept');
+assert.ok(!home.latest.some((x) => x.title === 'Ranked is pain'), 'a score-3 take without a name is not on the front page');
assert.strictEqual(home.trending[0].slug, 'supermarket-fridges-hacked-in-france');
assert.strictEqual(home.trending[0].slug, 'supermarket-fridges-hacked-in-france');
assert.strictEqual(home.trending[0].streamers, 2);
assert.strictEqual(home.trending[0].streamers, 2);
assert.deepStrictEqual(Object.keys(home.latest[0].user).sort(), ['avatar_url', 'display_name', 'id', 'profile_color', 'username'], 'a person is five fields, never a users row');
assert.deepStrictEqual(Object.keys(home.latest[0].user).sort(), ['avatar_url', 'display_name', 'id', 'profile_color', 'username'], 'a person is five fields, never a users row');