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)
Changes
@@@ -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),@@@ -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`);}@@@ -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) {@@@ -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,@@@ -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 }));
@@ -433,7 +433,7 @@ async function tick(opts = {}) {
@@ -433,7 +433,7 @@ async function tick(opts = {}) {
slug, user_id: v.user_id,
slug, user_id: v.user_id,
title: title.slice(0, 80), content, language: 'text', visibility: 'public',
title: title.slice(0, 80), content, language: 'text', visibility: 'public',
stream_id: v.stream_id, metadata,
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: {
screenshot: {
buffer: fs.readFileSync(screenshotPath),
buffer: fs.readFileSync(screenshotPath),
filename: path.basename(screenshotPath),
filename: path.basename(screenshotPath),
@@ -127,7 +127,7 @@ async function tick() {
@@ -127,7 +127,7 @@ async function tick() {
if (!r || !r.description) continue;
if (!r || !r.description) continue;
await require('../pastes-client').setPasteAi(p.slug, {
await require('../pastes-client').setPasteAi(p.slug, {
ai_summary: db.noLooks(r.description),
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));
}).catch((e) => console.warn('[AI backfill] paste ai write:', e.message));
console.log(`[AI backfill] paste ${p.slug} summarized`);
console.log(`[AI backfill] paste ${p.slug} summarized`);
}
}
@@ -207,7 +207,7 @@ async function _maybeLivePaste(stream, image, r, offset) {
@@ -207,7 +207,7 @@ async function _maybeLivePaste(stream, image, r, offset) {
slug: moments.makeSlug ? moments.makeSlug() : undefined, user_id: stream.user_id,
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',
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 }),
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' },
screenshot: { buffer, filename: `live-${stream.id}-${offset}.jpg`, contentType: 'image/jpeg' },
}, { origin: 'ai' });
}, { origin: 'ai' });
if (paste) {
if (paste) {
@@ -415,6 +415,12 @@ function _noClothing(text) {
@@ -415,6 +415,12 @@ function _noClothing(text) {
.replace(/,(?=[.;:!?])/g, '')
.replace(/,(?=[.;:!?])/g, '')
.replace(/ {2,}/g, ' '))).join('');
.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) {
function _noLooks(text) {
if (!text || typeof text !== 'string') return text;
if (!text || typeof text !== 'string') return text;
return _noClothing(text)
return _noClothing(text)
@@ -3550,7 +3556,7 @@ module.exports = {
@@ -3550,7 +3556,7 @@ module.exports = {
setVodAiOverview, setClipAiOverview, setVodTranscript, setClipTranscript, getStreamMemoriesInRange,
setVodAiOverview, setClipAiOverview, setVodTranscript, setClipTranscript, getStreamMemoriesInRange,
getVodsNeedingOverview, getClipsNeedingOverview, getVodsNeedingTimeline, getVodsNeedingTranscript, getClipsNeedingTranscript, getMediaNeedingAiAnalysis,
getVodsNeedingOverview, getClipsNeedingOverview, getVodsNeedingTimeline, getVodsNeedingTranscript, getClipsNeedingTranscript, getMediaNeedingAiAnalysis,
setVodTranscriptStatus, setClipTranscriptStatus, bumpVodTranscriptAttempt, bumpClipTranscriptAttempt,
setVodTranscriptStatus, setClipTranscriptStatus, bumpVodTranscriptAttempt, bumpClipTranscriptAttempt,
- cleanupMalformedAiText, noLooks: _noLooks, recordAiUsage, getAiCostToday, getAiCostTodayForUser, getAiUsageSummary,
+ cleanupMalformedAiText, noLooks: _noLooks, noLooksTags: _noLooksTags, recordAiUsage, getAiCostToday, getAiCostTodayForUser, getAiUsageSummary,
getStreamMemoriesByUser, countStreamMemoriesByUser, getStreamTranscriptSegments,
getStreamMemoriesByUser, countStreamMemoriesByUser, getStreamTranscriptSegments,
addTimelineEvents, getTimeline, getTimelineText, getTimelineCoverage, linkTimelineToVod, getTimelineByVod, getTimelineVodId,
addTimelineEvents, getTimeline, getTimelineText, getTimelineCoverage, linkTimelineToVod, getTimelineByVod, getTimelineVodId,
upsertStreamerOverview, getStreamerOverview, getAllStreamerOverviews, getStreamersNeedingOverview,
upsertStreamerOverview, getStreamerOverview, getAllStreamerOverviews, getStreamersNeedingOverview,
@@ -57,6 +57,9 @@ const db = require('../server/db/database');
@@ -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.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');
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.
// 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'];
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 }));
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 }));