openvibers/openvibe.live Commits

Public AI titles: body words count as looks; a refused clip title becomes "<stream title> at mm:ss" (#364)

OP OpenVibers committed 19 days ago · 92f61307c6 · 4 files, +18 −9

The first run of the clip titler (Live#363) named clip 488 "Boobs by Bobs Chalk Talk", from a sign in the stream: no
filter knew body words. They join the looks words every public AI title passes (moments, auto-clips, stream memory,
clip titles). A refused clip title falls back to the stream's title and the moment, not the overview's first clause.


Claude-Session: https://claude.ai/code/session_01K8AYn3FF3HE7CPfM262qdh

Co-authored-by: Claude Opus 5.5 <[email protected]>

Changes

ddiff --git a/server/ai/ai-moments-job.js b/server/ai/ai-moments-job.js
iindex 41a77d63..6c1a8e13 100644
---- a/server/ai/ai-moments-job.js
++++ b/server/ai/ai-moments-job.js
@@@ -59,7 +59,7 @@ function _deJson(desc) {
// dangling word ("…Speaking To") and never about how someone looks (owner's rule for public AI text: "A Man With A
// Beard Is Speaking To" and "A Person In A Bright Yellow High-Visibility Jacket" titled public clips, 2026-10-17).
// Otherwise the stream's own title and the moment's time ("Fortnite! at 41:20").
--const LOOKS_WORDS = /\b(wearing|wears|dressed|beard(ed)?|mustache|glasses|sunglasses|jacket|hoodie|sweater|vest|shirt|shirtless|topless|naked|nude|tank top|t-shirt|beanie|cap|hat|helmet|headset|headphones|hair|bald|blonde|expression|face|smil(e|ing)|clothing|outfit|tattoo(ed)?|young|old(er)?|teen(age)?|boy|girl|kid|child|blurred|body|skin)\b/i;
++const LOOKS_WORDS = /\b(wearing|wears|dressed|beard(ed)?|mustache|glasses|sunglasses|jacket|hoodie|sweater|vest|shirt|shirtless|topless|naked|nude|tank top|t-shirt|beanie|cap|hat|helmet|headset|headphones|hair|bald|blonde|expression|face|smil(e|ing)|clothing|outfit|tattoo(ed)?|young|old(er)?|teen(age)?|boy|girl|kid|child|blurred|body|skin|boob(s|ies)?|tits|titties|breasts?|cleavage|nipples?|butts?|booty|thighs?|crotch|sexy|busty|curvy|thicc)\b/i;
// A title also fails the shared rule (openvibe-sdk/no-looks, db.hasLooks): this list alone let "A Naked Man Lying On
// Their Back With Legs" through to Live's home (clip 483, 2026-10-23): it named neither "man" nor "naked".
const namesLooks = (s) => LOOKS_WORDS.test(s) || db.hasLooks(s);
ddiff --git a/server/ai/clip-title.js b/server/ai/clip-title.js
iindex 1acc53c0..31d57600 100644
---- a/server/ai/clip-title.js
++++ b/server/ai/clip-title.js
@@@ -4,8 +4,8 @@
* migrations/0019_clip_ai_title.sql). A clip keeps the title its maker gave it. One still named by a placeholder
* ("Untitled Clip", "Clip from stream", …: what the Clip buttons send when nobody types a title) is titled from its AI
* overview once that exists: OpenVibe.AI's live.stream.titles template (3-6 words, what happens, never what anyone looks
-- * like), then Live's rule for public AI titles (ai-moments-job publicTitle: a title naming looks is replaced by one from
-- * the overview's first clause, or the stream's title and the moment). The new title goes to OpenVibe.Media, and each
++ * like), then Live's rule for public AI titles (ai-moments-job publicTitle: a title naming looks or bodies, or a rough
++ * one, is replaced by the stream's title and the moment). The new title goes to OpenVibe.Media, and each
* clip is decided once (clip_ai_state.title_status: titled, kept or failed).
*
* isPlaceholder(title, sourceTitles) → true for a title nobody chose
@@@ -84,8 +84,9 @@ async function titleClips({ ids = null } = {}) {
if (raw == null) { if (out) await db.run("UPDATE clip_ai_state SET title_status = 'failed' WHERE clip_id = ?", [t.clip.id]); continue; }
const vod = { title: t.stream && t.stream.title, username: t.clip.username || (t.stream && t.stream.username) };
const at = Number(t.clip.start_time) || 0;
-- let title = String(publicTitle(raw, vod, at, t.state.ai_overview) || '').slice(0, 80).trim();
-- // Titles are public everywhere a clip shows: one with a slur or strong profanity gives way to the stream's.
++ // Titles are public everywhere a clip shows: one about looks or bodies, or with a slur or strong profanity,
++ // gives way to "<stream title> at mm:ss" (an overview's first clause makes a poor title: "The video centers on…").
++ let title = String(publicTitle(raw, vod, at, null) || '').slice(0, 80).trim();
if (title && !frontPageOk(title)) title = String(publicTitle('', vod, at, null) || '').slice(0, 80).trim();
// The maker may have named it while the AI was writing: their title wins.
let fresh = null;
ddiff --git a/test/clip-title.test.js b/test/clip-title.test.js
iindex a5177e6a..0d9ee126 100644
---- a/test/clip-title.test.js
++++ b/test/clip-title.test.js
@@@ -48,7 +48,7 @@ console.warn = () => {};
media.updateClip = async (id, fields) => { updates.push([Number(id), fields.title]); clips.get(Number(id)).title = fields.title; return { id }; };
const calls = [];
-- const answers = { 'triple kill': 'Triple Kill Final Round Clutch', 'dress': 'Woman In Red Dress Dances', 'rough clutch': 'Fucking Insane Clutch', 'rename': 'Late Game Comeback', 'default': 'Shotgun Rush Through Hallway' };
++ const answers = { 'triple kill': 'Triple Kill Final Round Clutch', 'dress': 'Woman In Red Dress Dances', 'painted': 'Boobs by Bobs Chalk Talk', 'rough clutch': 'Fucking Insane Clutch', 'rename': 'Late Game Comeback', 'default': 'Shotgun Rush Through Hallway' };
aiService.structured = async (workflow, input, opts) => {
calls.push({ workflow, input, opts });
return { titles: input.summaries.map((s, index) => {
@@@ -70,6 +70,7 @@ console.warn = () => {};
put(605, 'Clip from stream'); await seed(605, 'Someone in a red dress dances on the stage while chat cheers.');
put(606, 'Untitled Clip'); await seed(606, 'A rough clutch play in the last seconds.');
await seed(607, 'Overview of a clip Media no longer has.');
++ put(612, 'Clip from stream'); await seed(612, 'A streamer shows chalkboard signs for "Boobs Painted by Bobs", an art collection.');
put(608, 'Untitled Clip'); await seed(608, 'A rename test: a late game comeback.');
reads.set(608, ['Untitled Clip', 'What a comeback']); // the person names it before the AI's title is saved
@@@ -79,17 +80,18 @@ console.warn = () => {};
assert.strictEqual(calls[0].workflow, 'live.stream.titles');
assert.strictEqual(calls[0].opts.meter.kind, 'clip_titles');
const summaries = calls[0].input.summaries;
-- assert.ok(summaries.length === 5 && summaries.every((s) => s.length <= 1000), 'one summary per placeholder clip with an overview');
++ assert.ok(summaries.length === 6 && summaries.every((s) => s.length <= 1000), 'one summary per placeholder clip with an overview');
const s601 = summaries.find((s) => s.includes('triple kill'));
assert.ok(s601.includes('Said: "no way, no way!"') && s601.includes('From the stream "Friday ranked" (Shooters)'), s601);
const byId = new Map(updates);
assert.strictEqual(byId.get(601), 'Triple Kill Final Round Clutch');
assert.strictEqual(byId.get(603), 'Shotgun Rush Through Hallway', '"Clip: <stream title>" is a placeholder');
-- assert.ok(byId.has(605) && !/dress|woman/i.test(byId.get(605)), `a title naming looks is replaced: ${byId.get(605)}`);
++ assert.strictEqual(byId.get(605), 'Friday ranked at 0:42', 'a title naming looks gives way to the stream and the moment');
assert.strictEqual(byId.get(606), 'Friday ranked at 0:42', 'a rough title gives way to the stream and the moment');
++ assert.strictEqual(byId.get(612), 'Friday ranked at 0:42', 'a body word gives way too (clip 488, 2026-10-25)');
assert.ok(!byId.has(602) && !byId.has(604) && !byId.has(607) && !byId.has(608), 'nothing else is written to Media');
-- assert.strictEqual(n, 4);
++ assert.strictEqual(n, 5);
assert.deepStrictEqual(
[await status(601), await status(602), await status(603), await status(604), await status(605), await status(606), await status(607), await status(608)],
ddiff --git a/test/moment-fallback-title.test.js b/test/moment-fallback-title.test.js
iindex 55ec44f5..05619d29 100644
---- a/test/moment-fallback-title.test.js
++++ b/test/moment-fallback-title.test.js
@@@ -32,6 +32,12 @@ assert.strictEqual(publicTitle('Red Beanie Moment', vod, 2480, 'A man wearing a
assert.strictEqual(publicTitle('A Naked Man Lying On Their Back With Legs', vod, 2480, ''), 'Fortnite! at 41:20');
assert.strictEqual(publicTitle('Woman Laughs At Chat', vod, 2480, ''), 'Fortnite! at 41:20', 'a guessed gender is a look');
assert.strictEqual(captionTitle('A naked man lies on a bed near a window.'), null);
++// Body words too: clip 488 was AI-titled "Boobs by Bobs Chalk Talk" from a sign in the stream (2026-10-25).
++assert.strictEqual(publicTitle('Boobs by Bobs Chalk Talk', vod, 2480, ''), 'Fortnite! at 41:20');
++assert.strictEqual(publicTitle('Sexy Dance Break', vod, 2480, ''), 'Fortnite! at 41:20');
++assert.strictEqual(publicTitle('Thighs and Cleavage Cam', vod, 2480, ''), 'Fortnite! at 41:20');
++assert.strictEqual(publicTitle('Butt Slide Fail', vod, 2480, ''), 'Fortnite! at 41:20');
++assert.strictEqual(publicTitle('Kick-Ass Combo Finish', vod, 60, ''), 'Kick-Ass Combo Finish', 'not every rough word is a body word');
assert.strictEqual(publicTitle('Headphones Hit a Revelation', { title: 'A little couch surfing before the store.' }, 61, ''), 'A little couch surfing before the store at 1:01');
assert.strictEqual(publicTitle('', { title: '', username: 'goosely' }, 75, ''), "goosely's stream at 1:15");
process.stdout.write('moment fallback title: all checks passed\n');