openvibers/openvibe.live Commits

AI tags on public items drop looks too ('red-beanie', 'bearded', 'man', 'tanktop'): live pastes, moment pastes and backfilled paste tags pass db.noLooksTags (#203)

OP OpenVibers committed 27 days ago · 2fa95a0fc7 · 5 files, +13 −4

Changes

ddiff --git a/server/ai/ai-moments-job.js b/server/ai/ai-moments-job.js
iindex 72cfb44e..6c96cbc6 100644
---- a/server/ai/ai-moments-job.js
++++ b/server/ai/ai-moments-job.js
@@@ -433,7 +433,7 @@ async function tick(opts = {}) {
slug, user_id: v.user_id,
title: title.slice(0, 80), content, language: 'text', visibility: 'public',
stream_id: v.stream_id, metadata,
-- ai_summary: desc, ai_tags: JSON.stringify(tags.length ? tags : []),
++ ai_summary: desc, ai_tags: JSON.stringify(db.noLooksTags(tags)),
screenshot: {
buffer: fs.readFileSync(screenshotPath),
filename: path.basename(screenshotPath),
ddiff --git a/server/ai/backfill-job.js b/server/ai/backfill-job.js
iindex 2a99def9..b5435811 100644
---- a/server/ai/backfill-job.js
++++ b/server/ai/backfill-job.js
@@@ -127,7 +127,7 @@ async function tick() {
if (!r || !r.description) continue;
await require('../pastes-client').setPasteAi(p.slug, {
ai_summary: db.noLooks(r.description),
-- ai_tags: JSON.stringify(r.tags || []),
++ ai_tags: JSON.stringify(db.noLooksTags(r.tags || [])),
}).catch((e) => console.warn('[AI backfill] paste ai write:', e.message));
console.log(`[AI backfill] paste ${p.slug} summarized`);
}
ddiff --git a/server/ai/stream-memory-job.js b/server/ai/stream-memory-job.js
iindex e47b89e3..47e7f989 100644
---- a/server/ai/stream-memory-job.js
++++ b/server/ai/stream-memory-job.js
@@@ -207,7 +207,7 @@ async function _maybeLivePaste(stream, image, r, offset) {
slug: moments.makeSlug ? moments.makeSlug() : undefined, user_id: stream.user_id,
title, content: `${desc}\n\n🔴 Caught live on @${username}'s stream — ${base}/@${encodeURIComponent(username)}`, language: 'text', visibility: 'public',
stream_id: stream.id, metadata: JSON.stringify({ ai_moment: true, live: true, stream_id: stream.id, offset, username, vod_link: null }),
-- ai_summary: desc, ai_tags: JSON.stringify(Array.isArray(r.tags) ? r.tags.slice(0, 8) : []),
++ ai_summary: desc, ai_tags: JSON.stringify(db.noLooksTags(Array.isArray(r.tags) ? r.tags.slice(0, 8) : [])),
screenshot: { buffer, filename: `live-${stream.id}-${offset}.jpg`, contentType: 'image/jpeg' },
}, { origin: 'ai' });
if (paste) {
ddiff --git a/server/db/database.js b/server/db/database.js
iindex 74c7a6b6..741cb947 100644
---- a/server/db/database.js
++++ b/server/db/database.js
@@@ -415,6 +415,12 @@ function _noClothing(text) {
.replace(/,(?=[.;:!?])/g, '')
.replace(/ {2,}/g, ' '))).join('');
}
++// Tags shown with public AI items ("red-beanie", "bearded", "man", "tanktop") follow the same rule: a tag naming
++// clothing, hair, face, age or a person by gender is dropped.
++const _LOOKS_TAG = new RegExp(`(?:^|[\\s_-])(?:${_WORN}|${_FACE}|${_LOOKS}|men|women|man|woman|boy|girl|guy|gal|kid|child|teen|teenager|lady|male|female|youth|shirtless|topless|tanktop|selfie)(?=$|[\\s_-])`, 'i');
++function _noLooksTags(tags) {
++ return (Array.isArray(tags) ? tags : []).filter((t) => typeof t === 'string' && t.trim() && !_LOOKS_TAG.test(t.trim()));
++}
function _noLooks(text) {
if (!text || typeof text !== 'string') return text;
return _noClothing(text)
@@@ -3550,7 +3556,7 @@ module.exports = {
setVodAiOverview, setClipAiOverview, setVodTranscript, setClipTranscript, getStreamMemoriesInRange,
getVodsNeedingOverview, getClipsNeedingOverview, getVodsNeedingTimeline, getVodsNeedingTranscript, getClipsNeedingTranscript, getMediaNeedingAiAnalysis,
setVodTranscriptStatus, setClipTranscriptStatus, bumpVodTranscriptAttempt, bumpClipTranscriptAttempt,
-- cleanupMalformedAiText, noLooks: _noLooks, recordAiUsage, getAiCostToday, getAiCostTodayForUser, getAiUsageSummary,
++ cleanupMalformedAiText, noLooks: _noLooks, noLooksTags: _noLooksTags, recordAiUsage, getAiCostToday, getAiCostTodayForUser, getAiUsageSummary,
getStreamMemoriesByUser, countStreamMemoriesByUser, getStreamTranscriptSegments,
addTimelineEvents, getTimeline, getTimelineText, getTimelineCoverage, linkTimelineToVod, getTimelineByVod, getTimelineVodId,
upsertStreamerOverview, getStreamerOverview, getAllStreamerOverviews, getStreamersNeedingOverview,
ddiff --git a/test/ai-text-no-looks.test.js b/test/ai-text-no-looks.test.js
iindex 0911e2bf..c640599c 100644
---- a/test/ai-text-no-looks.test.js
++++ b/test/ai-text-no-looks.test.js
@@@ -57,6 +57,9 @@ const db = require('../server/db/database');
assert.deepStrictEqual(Object.values(await db.get('SELECT ai_overview, ai_overview_short FROM streams WHERE id = ?', [sid])), ['The streamer plays Fortnite.', 'The streamer plays Fortnite.']);
assert.strictEqual((await db.get('SELECT description FROM stream_memories WHERE offset_seconds = 9060')).description, 'An older version of the game loads.', 'looks words about things stay');
++ // Tags shown with public AI items drop clothing, hair, face, age and gendered people.
++ assert.deepStrictEqual(db.noLooksTags(['red-beanie', 'bearded', 'man', 'tanktop', 'hula hoop', 'laptop', 'minecraft', 'outdoors', 'jacket', 'selfie']), ['hula hoop', 'laptop', 'minecraft', 'outdoors']);
++
// The home hero's AI copy: a line about someone's looks is never served, even from a batch stored before.
const words = ['crouton connoisseurs', 'van-dwelling coders', 'ara007 shirtless thoughts', 'goosely fans', 'late-night tinkerers', 'young gamers'];
await db.setState('home_hero_slogans', JSON.stringify({ audiences: words, quips: ['chat is cooking', 'a shirtless take', 'goosely did it again', 'rust compiles eventually', 'one more build'], updated_at: Date.now(), v: 4 }));