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)
Changes
nnew file mode 100644@@@ -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 };@@@ -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) {@@@ -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.nnew file mode 100644@@@ -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');@@@ -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');
new file mode 100644
new file mode 100644
@@ -0,0 +1,27 @@
@@ -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 };
@@ -35,15 +35,14 @@ function _stripAudiencePrefix(s) {
@@ -35,15 +35,14 @@ function _stripAudiencePrefix(s) {
.trim();
.trim();
}
}
const _BAD_AUDIENCE = /\b(live\s*-?\s*)?stream(ing|s)?\b|\blivestream/i;
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) {
function _cleanList(arr, maxLen, max) {
if (!Array.isArray(arr)) return [];
if (!Array.isArray(arr)) return [];
const seen = new Set(); const out = [];
const seen = new Set(); const out = [];
for (const raw of arr) {
for (const raw of arr) {
let s = String(raw == null ? '' : raw).trim().replace(/^["'‘’“”\-•\s]+|["'‘’“”\s]+$/g, '').replace(/[.,;:]+$/, '');
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();
const k = s.toLowerCase();
if (seen.has(k)) continue;
if (seen.has(k)) continue;
seen.add(k); out.push(s);
seen.add(k); out.push(s);
@@ -53,7 +52,7 @@ function _cleanList(arr, maxLen, max) {
@@ -53,7 +52,7 @@ function _cleanList(arr, maxLen, max) {
}
}
function _topUp(fresh, old, cap) {
function _topUp(fresh, old, cap) {
const seen = new Set(fresh.map(s => s.toLowerCase())); const out = fresh.slice();
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);
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)
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() {
@@ -125,15 +124,13 @@ async function tick() {
}, { meter: { kind: 'hero_slogans', role: 'legacy' } });
}, { meter: { kind: 'hero_slogans', role: 'legacy' } });
if (!parsed) return;
if (!parsed) return;
let audiences = _cleanList((parsed.audiences || []).map(_stripAudiencePrefix).filter(a => a && !_BAD_AUDIENCE.test(a) && !/^for\b/i.test(a)), 60, TARGET);
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
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.
// Fresh daily batch; top up from the previous batch only if the model returned few.
const old = await _loadPool();
const old = await _loadPool();
audiences = _topUp(audiences, old.audiences.map(_stripAudiencePrefix).filter(a => a && !_BAD_AUDIENCE.test(a)), TARGET);
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() }));
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)`);
console.log(`[Slogans] Fresh daily batch: ${audiences.length} words, ${quips.length} slogans (from full AI context)`);
} catch (e) {
} catch (e) {
@@ -134,9 +134,8 @@ function _cleanAudience(raw) {
@@ -134,9 +134,8 @@ function _cleanAudience(raw) {
.trim();
.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() {
async function heroSlogans() {
let s = null;
let s = null;
@@ -144,9 +143,9 @@ async function heroSlogans() {
@@ -144,9 +143,9 @@ async function heroSlogans() {
s = await db.getState('home_hero_slogans');
s = await db.getState('home_hero_slogans');
if (typeof s === 'string') s = JSON.parse(s);
if (typeof s === 'string') s = JSON.parse(s);
} catch { s = null; }
} 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 hasAi = aiAud.length >= 4 || aiQuips.length >= 4;
const updatedAt = (s && Number(s.updated_at)) || 0;
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.
// The slogan job regenerates every 12h; next_at is when the current batch is due to refresh.
new file mode 100644
new file mode 100644
@@ -0,0 +1,19 @@
@@ -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');
@@ -33,5 +33,9 @@ for (const list of ['FALLBACK_QUIPS', 'FALLBACK_AUDIENCES']) {
@@ -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');
assert.ok(!NO_FREE.test(/tagline: '([^']*)'/.exec(read('public/js/footer-live.js'))[1]), 'footer tagline');
const slogan = read('server/ai/slogan-job.js');
const slogan = read('server/ai/slogan-job.js');
assert.ok(!slogan.includes('Free & Open'), 'the slogan prompt does not ask for "free"');
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');
console.log('shipped-live: all checks passed');