openvibers/openvibe.live Commits

Home hero: the AI copy rules live in one place (server/ai/hero-copy.js) and also refuse crude words, keyboard-mash names and viewer-count jokes ('testicle banter crew', 'qeqweqwe watchers', "goosely's loyal 3 viewers" were on the front page) (#195)

OP OpenVibers committed 27 days ago · eb0bc8f4b4 · 5 files, +62 −16

* Home hero: the AI copy rules live in one place (server/ai/hero-copy.js) and also refuse crude words, keyboard-mash names and viewer-count jokes ('testicle banter crew', 'qeqweqwe watchers', "goosely's loyal 3 viewers" were on the front page)

* Hero copy: keep the channel's own '3 viewers' joke (it is the prompt's example); only crude words and keyboard-mash names join the rules

Changes

ddiff --git a/server/ai/hero-copy.js b/server/ai/hero-copy.js
nnew file mode 100644
iindex 00000000..4970070e
---- /dev/null
++++ b/server/ai/hero-copy.js
@@@ -0,0 +1,27 @@
++'use strict';
++/**
++ * What the home hero's AI copy may say ("Live streaming for {audience}" and the quips under it). The slogan job
++ * (server/ai/slogan-job.js) writes it from real chat and streams, so it names real people, and it is the first line
++ * every visitor reads. Checked when a batch is stored and again when the hero serves one, so a rule added later
++ * also cleans a batch stored before it.
++ *
++ * publicOk(text) false when the text breaks any rule below
++ */
++
++// The owner's rule: no "free"/"$0" copy anywhere.
++const NO_FREE = /\b(free|no[- ]cost)\b|\$\s?0\b/i;
++// Never a real person's looks or age ("ara007 shirtless thoughts" was served once).
++const LOOKS = /\b(shirtless|topless|bare-?chested|naked|nude|cleavage|bikini|underwear|teen(age)?|young|kids?|child(ren)?|boys?|girls?)\b/i;
++// Nothing crude or sexual on the front page ("testicle banter crew", 2026-10-17), whatever chat joked about.
++const CRUDE = /\b(testic\w*|dick|cock|penis|vagina|puss(y|ies)|boobs?|tits|titties|ass(hole)?s?|butthole|cum|horny|sexy?|porn\w*|nsfw|fuck\w*|shit\w*|piss\w*|bitch\w*|cunt|wank\w*|poop\w*|fart\w*|turds?)\b/i;
++// A keyboard mash taken for a name ("qeqweqwe watchers").
++const MASH = /(qwe|ewq|asdf|sdfg|zxcv|qeqw)/i;
++
++const RULES = [NO_FREE, LOOKS, CRUDE, MASH];
++
++function publicOk(text) {
++ const s = String(text == null ? '' : text);
++ return !RULES.some((re) => re.test(s));
++}
++
++module.exports = { publicOk, NO_FREE, LOOKS, CRUDE, MASH };
ddiff --git a/server/ai/slogan-job.js b/server/ai/slogan-job.js
iindex 5000cab1..96370c53 100644
---- a/server/ai/slogan-job.js
++++ b/server/ai/slogan-job.js
@@@ -35,15 +35,14 @@ function _stripAudiencePrefix(s) {
.trim();
}
const _BAD_AUDIENCE = /\b(live\s*-?\s*)?stream(ing|s)?\b|\blivestream/i;
--const _NO_FREE = /\b(free|no[- ]cost)\b|\$\s?0\b/i;
--// Copy about a real person never mentions their looks or age ("ara007 shirtless thoughts" made the hero once).
--const _LOOKS = /\b(shirtless|topless|bare-?chested|naked|nude|cleavage|bikini|underwear|teen(age)?|young|kids?|child(ren)?|boys?|girls?)\b/i;
++// What the hero may say (no "free", nothing about looks, nothing crude, no keyboard-mash names).
++const { publicOk } = require('./hero-copy');
function _cleanList(arr, maxLen, max) {
if (!Array.isArray(arr)) return [];
const seen = new Set(); const out = [];
for (const raw of arr) {
let s = String(raw == null ? '' : raw).trim().replace(/^["'‘’“”\-•\s]+|["'‘’“”\s]+$/g, '').replace(/[.,;:]+$/, '');
-- if (!s || s.length > maxLen || _LOOKS.test(s)) continue;
++ if (!s || s.length > maxLen || !publicOk(s)) continue;
const k = s.toLowerCase();
if (seen.has(k)) continue;
seen.add(k); out.push(s);
@@@ -53,7 +52,7 @@ function _cleanList(arr, maxLen, max) {
}
function _topUp(fresh, old, cap) {
const seen = new Set(fresh.map(s => s.toLowerCase())); const out = fresh.slice();
-- for (const s of (old || [])) { const v = String(s || '').trim(); if (!v || _LOOKS.test(v)) continue; const k = v.toLowerCase(); if (seen.has(k)) continue; seen.add(k); out.push(v); if (out.length >= cap) break; }
++ for (const s of (old || [])) { const v = String(s || '').trim(); if (!v || !publicOk(v)) continue; const k = v.toLowerCase(); if (seen.has(k)) continue; seen.add(k); out.push(v); if (out.length >= cap) break; }
return out.slice(0, cap);
}
const SLOGAN_FORMAT = 4; // bump to force a one-time regen after a prompt/format change (4: no copy about looks)
@@@ -125,15 +124,13 @@ async function tick() {
}, { meter: { kind: 'hero_slogans', role: 'legacy' } });
if (!parsed) return;
let audiences = _cleanList((parsed.audiences || []).map(_stripAudiencePrefix).filter(a => a && !_BAD_AUDIENCE.test(a) && !/^for\b/i.test(a)), 60, TARGET);
-- // The owner's rule: no "free"/"$0" copy anywhere, whatever the model writes.
-- audiences = audiences.filter(a => !_NO_FREE.test(a));
-- let quips = _cleanList((parsed.quips || []).filter(q => !_NO_FREE.test(String(q))), 110, TARGET);
++ let quips = _cleanList(parsed.quips || [], 110, TARGET);
if (audiences.length < 6 && quips.length < 6) return; // bad batch — keep yesterday's
// Fresh daily batch; top up from the previous batch only if the model returned few.
const old = await _loadPool();
audiences = _topUp(audiences, old.audiences.map(_stripAudiencePrefix).filter(a => a && !_BAD_AUDIENCE.test(a)), TARGET);
-- quips = _topUp(quips, old.quips.filter(q => !_NO_FREE.test(String(q))), TARGET);
++ quips = _topUp(quips, old.quips, TARGET);
await db.setState('home_hero_slogans', JSON.stringify({ v: SLOGAN_FORMAT, audiences, quips, updated_at: Date.now() }));
console.log(`[Slogans] Fresh daily batch: ${audiences.length} words, ${quips.length} slogans (from full AI context)`);
} catch (e) {
ddiff --git a/server/home/routes.js b/server/home/routes.js
iindex 5da42311..242d59bf 100644
---- a/server/home/routes.js
++++ b/server/home/routes.js
@@@ -134,9 +134,8 @@ function _cleanAudience(raw) {
.trim();
}
--const NO_FREE = /\b(free|no[- ]cost)\b|\$\s?0\b/i;
--// Nor copy about a real person's looks or age ("ara007 shirtless thoughts" was served once).
--const LOOKS = /\b(shirtless|topless|bare-?chested|naked|nude|cleavage|bikini|underwear|teen(age)?|young|kids?|child(ren)?|boys?|girls?)\b/i;
++// What the hero may say: server/ai/hero-copy.js (no "free", nothing about looks, nothing crude, no keyboard-mash names).
++const { publicOk } = require('../ai/hero-copy');
async function heroSlogans() {
let s = null;
@@@ -144,9 +143,9 @@ async function heroSlogans() {
s = await db.getState('home_hero_slogans');
if (typeof s === 'string') s = JSON.parse(s);
} catch { s = null; }
-- // No "free"/"$0" copy (the owner's rule) and none about looks, even in a batch stored before the slogan job filtered it.
-- const aiAud = (s && Array.isArray(s.audiences)) ? s.audiences.map(_cleanAudience).filter(a => a && a.length <= 60 && !NO_FREE.test(a) && !LOOKS.test(a)) : [];
-- const aiQuips = (s && Array.isArray(s.quips)) ? s.quips.filter(q => q && String(q).length <= 120 && !NO_FREE.test(String(q)) && !LOOKS.test(String(q))) : [];
++ // The hero-copy rules again here, so a batch stored before a rule existed is cleaned when it is served.
++ const aiAud = (s && Array.isArray(s.audiences)) ? s.audiences.map(_cleanAudience).filter(a => a && a.length <= 60 && publicOk(a)) : [];
++ const aiQuips = (s && Array.isArray(s.quips)) ? s.quips.filter(q => q && String(q).length <= 120 && publicOk(q)) : [];
const hasAi = aiAud.length >= 4 || aiQuips.length >= 4;
const updatedAt = (s && Number(s.updated_at)) || 0;
// The slogan job regenerates every 12h; next_at is when the current batch is due to refresh.
ddiff --git a/test/hero-copy.test.js b/test/hero-copy.test.js
nnew file mode 100644
iindex 00000000..0eea4fe1
---- /dev/null
++++ b/test/hero-copy.test.js
@@@ -0,0 +1,19 @@
++'use strict';
++/**
++ * The home hero's AI copy rules (server/ai/hero-copy.js): the 2026-10-17 batch put "testicle banter crew" and
++ * "qeqweqwe watchers" on the front page. Those, "free" copy and copy about looks are refused; ordinary audiences and
++ * quips pass, and so does "goosely’s loyal 3 viewers" (the prompt's own example of a joke the channel makes about
++ * itself).
++ *
++ * node test/hero-copy.test.js
++ */
++const assert = require('assert');
++const { publicOk } = require('../server/ai/hero-copy');
++
++const refused = ['testicle banter crew', 'qeqweqwe watchers', 'free streaming fans', 'ara007 shirtless thoughts',
++ 'sexy stream squad', 'shitpost central', 'asdfgh chill crew'];
++for (const s of refused) assert.strictEqual(publicOk(s), false, `refused: ${s}`);
++const kept = ['2fat’s hype squad', 'JustTwoCuteBesties fans', 'Maticus and crew', 'JessePlays rhythm lovers', 'meme spike mob', 'van-dwelling coders',
++ 'multi-GPU benchmark junkies', 'goosely’s loyal 3 viewers', 'class clowns', 'button mashers', 'from Tesla GPUs to spicy memes', 'throttle the hype, not the vibes', 'Goosely chaos chat'];
++for (const s of kept) assert.strictEqual(publicOk(s), true, `kept: ${s}`);
++console.log('hero copy: all checks passed');
ddiff --git a/test/shipped-live.test.js b/test/shipped-live.test.js
iindex 0e5915e2..2e6dbb29 100644
---- a/test/shipped-live.test.js
++++ b/test/shipped-live.test.js
@@@ -33,5 +33,9 @@ for (const list of ['FALLBACK_QUIPS', 'FALLBACK_AUDIENCES']) {
assert.ok(!NO_FREE.test(/tagline: '([^']*)'/.exec(read('public/js/footer-live.js'))[1]), 'footer tagline');
const slogan = read('server/ai/slogan-job.js');
assert.ok(!slogan.includes('Free & Open'), 'the slogan prompt does not ask for "free"');
--assert.ok(/filter\(q => !_NO_FREE\.test/.test(slogan) && routes.includes('!NO_FREE.test(String(q))'), 'generated and stored slogans are filtered');
++// Generated (slogan job) and served (hero route) copy both pass server/ai/hero-copy.js, which refuses "free".
++const heroCopy = require('../server/ai/hero-copy');
++assert.ok(!heroCopy.publicOk('Free streaming for everyone') && !heroCopy.publicOk('$0 forever'), 'hero copy refuses "free"');
++assert.ok(slogan.includes("require('./hero-copy')") && /publicOk\(s\)/.test(slogan), 'generated slogans are filtered');
++assert.ok(routes.includes("require('../ai/hero-copy')") && routes.includes('publicOk(q)') && routes.includes('publicOk(a)'), 'stored slogans are filtered when served');
console.log('shipped-live: all checks passed');