openvibers/openvibe.live Commits
Public AI moments are the observation only: the raw speech-to-text clause (" — heard: …", up to 500 characters, often another language) stays for the AI's context and is never shown (home 'Right now', a VOD's AI timeline, a streamer's AI profile); pose and expression wording goes like looks (owner, 2026-10-17: a Greek stream's transcript filled the home page) (#236)
Changes
@@@ -47,7 +47,7 @@ router.get('/user/:id', async (req, res) => {// for more, and default high enough to read as a real timeline.const memLimit = Math.min(200, Math.max(1, parseInt(req.query.memories, 10) || 40));const mems = (await db.getStreamMemoriesByUser(uid, memLimit) || []).map(m => ({-- description: m.description || '',++ description: db.publicMemoryText(m.description),created_at: m.created_at,stream_id: m.stream_id,offset_seconds: m.offset_seconds,@@@ -433,9 +433,11 @@ const _hasLooks = (t) => _LOOKS_WORD.test(String(t || ''));function _noLooksTags(tags) {return (Array.isArray(tags) ? tags : []).filter((t) => typeof t === 'string' && t.trim() && !_LOOKS_TAG.test(t.trim()));}++// Body language is the person too ("reacting with a thoughtful pose", "in a relaxed posture"): the phrase goes.++const _POSE = /,?\s*(?:(?:appearing|seeming|seemingly)\s+(?:to\s+be\s+)?)?(?:(?:reacting|sitting|standing|leaning|posing|looking|resting)\s+)?(?:with|in)\s+(?:an?|their|his|her)\s+(?:[\w-]+\s+){0,2}(?:pose|posture|expression|facial expression|stance)\b/gi;function _noLooks(text) {if (!text || typeof text !== 'string') return text;-- return _noClothing(text)++ return _noClothing(text).replace(_POSE, '').replace(/\s+([,.;:!?])/g, '$1').replace(/ {2,}/g, ' ')// Looks named: the person on camera. Only an age ("a child sits nearby"): someone, who may not be the streamer..replace(_LOOKED_AT, (m, art, looks, noun) => (looks ? _cap(m, 'the streamer') : _AGE_NOUN.test(noun) ? _cap(m, /^the$/i.test(art) ? 'the person' : 'someone') : m)).replace(_LOOKED_AT_BARE, (m) => _cap(m, 'streamer'))@@@ -446,6 +448,16 @@ function _noLooks(text) {.replace(/\s*,\s*(?:shirtless|topless|bare-chested)\s*,/gi, '').replace(/\b(?:shirtless|topless|bare-chested)\b,?\s*/gi, '');}++/**++ * A stream memory as the public sees it (home "Right now", a VOD's AI timeline, a streamer's AI profile): the++ * observation only. The " — heard: "…"" clause is raw speech-to-text kept for the AI's own context (often another++ * language, misheard, 500 characters); shown, it read as a wall of garbled text (2026-10-17, a Greek stream on the++ * home page). Looks and pose wording go as everywhere public.++ */++function _publicMemoryText(text) {++ const t = String(text || '').replace(/\s+—\s+heard:[\s\S]*$/, '').trim();++ return (_noLooks(t) || '').trim();++}// Derive a concise short overview from a long one — the lead sentence(s), capped// ~150 chars at a sentence/word boundary. Deterministic + free (no AI call), so it// can be cached at write time and shown on listing cards.@@@ -3586,7 +3598,7 @@ module.exports = {setVodAiOverview, setClipAiOverview, setVodTranscript, setClipTranscript, getStreamMemoriesInRange,getVodsNeedingOverview, getClipsNeedingOverview, getVodsNeedingTimeline, getVodsNeedingTranscript, getClipsNeedingTranscript, getMediaNeedingAiAnalysis,setVodTranscriptStatus, setClipTranscriptStatus, bumpVodTranscriptAttempt, bumpClipTranscriptAttempt,-- cleanupMalformedAiText, noLooks: _noLooks, noLooksTags: _noLooksTags, hasLooks: _hasLooks, recordAiUsage, getAiCostToday, getAiCostTodayForUser, getAiUsageSummary,++ cleanupMalformedAiText, noLooks: _noLooks, noLooksTags: _noLooksTags, hasLooks: _hasLooks, publicMemoryText: _publicMemoryText, recordAiUsage, getAiCostToday, getAiCostTodayForUser, getAiUsageSummary,getStreamMemoriesByUser, countStreamMemoriesByUser, getStreamTranscriptSegments,addTimelineEvents, getTimeline, getTimelineText, getTimelineCoverage, linkTimelineToVod, getTimelineByVod, getTimelineVodId,upsertStreamerOverview, getStreamerOverview, getAllStreamerOverviews, getStreamersNeedingOverview,@@@ -273,7 +273,8 @@ router.get('/featured', async (req, res) => {let moment = null;try {const m = await db.getLatestStreamMemory(pick.id);-- if (m && m.description) moment = { description: m.description, captured_at: m.captured_at || m.created_at || null, thumbnail_url: m.thumbnail_url || null };++ const text = m && db.publicMemoryText(m.description);++ if (text) moment = { description: text, captured_at: m.captured_at || m.created_at || null, thumbnail_url: m.thumbnail_url || null };} catch { /* */ }res.json({ stream, moment, count: live.length, index: idx, next_in_ms: FEATURED_ROTATE_MS - (Date.now() % FEATURED_ROTATE_MS) });} catch (err) {@@@ -390,7 +390,7 @@ router.get('/:id/memories', optionalAuth, async (req, res) => {if (!streamId) return res.json({ memories: [] });const memories = (await db.getStreamMemories(streamId) || []).map(m => ({offset_seconds: m.offset_seconds,-- description: m.description,++ description: db.publicMemoryText(m.description),tags: m.tags ? (() => { try { return JSON.parse(m.tags); } catch { return []; } })() : [],}));res.json({ memories });@@@ -27,6 +27,10 @@ const db = require('../server/db/database');['The view is close to the camera while wearing a black beanie and hoodie, with trees around.', 'The view is close to the camera, with trees around.'],['A man in a gray tank top sits against a plain light background.', 'A man sits against a plain light background.'],['A man with a long beard and plaid shirt examines a woven item.', 'A man examines a woven item.'],++ // Pose and expression are the person too (2026-10-17, the home page's "Right now").++ ['A live stream frame shows the streamer in a small window bottom-left, appearing to be reacting with a thoughtful pose while the main screen is black.',++ 'A live stream frame shows the streamer in a small window bottom-left while the main screen is black.'],++ ['The streamer sits in a relaxed posture at the desk.', 'The streamer sits at the desk.'],['A kitchen studio with a headset-wearing host, microphone, and on-screen graphics.', 'A kitchen studio with a host, microphone, and on-screen graphics.'],['Panelists discuss while wearing headsets and microphones, with a trophy nearby.', 'Panelists discuss, with a trophy nearby.'],['A man wearing sunglasses on his head looks down at the camera.', 'A man looks down at the camera.'],@@@ -46,6 +50,9 @@ const db = require('../server/db/database');['The streamer laughs — heard: "Put your headset on. I like your hair."', 'The streamer laughs — heard: "Put your headset on. I like your hair."'],];let off = 0;++ // Public memory text is the observation alone: the raw transcript clause stays for the AI's context, never shown.++ assert.strictEqual(db.publicMemoryText('The main screen is black, suggesting a pause. — heard: "Δεν θα πούμε άλλο. Τώρα που κερδίζουμε."'), 'The main screen is black, suggesting a pause.');++ assert.strictEqual(db.publicMemoryText(null), '');for (const [input, want] of cases) {await db.addStreamMemory({ stream_id: sid, user_id: uid, offset_seconds: (off += 60), description: input });const row = await db.get('SELECT description FROM stream_memories WHERE stream_id = ? AND offset_seconds = ?', [sid, off]);
@@ -47,7 +47,7 @@ router.get('/user/:id', async (req, res) => {
@@ -47,7 +47,7 @@ router.get('/user/:id', async (req, res) => {
// for more, and default high enough to read as a real timeline.
// for more, and default high enough to read as a real timeline.
const memLimit = Math.min(200, Math.max(1, parseInt(req.query.memories, 10) || 40));
const memLimit = Math.min(200, Math.max(1, parseInt(req.query.memories, 10) || 40));
const mems = (await db.getStreamMemoriesByUser(uid, memLimit) || []).map(m => ({
const mems = (await db.getStreamMemoriesByUser(uid, memLimit) || []).map(m => ({
- description: m.description || '',
+ description: db.publicMemoryText(m.description),
created_at: m.created_at,
created_at: m.created_at,
stream_id: m.stream_id,
stream_id: m.stream_id,
offset_seconds: m.offset_seconds,
offset_seconds: m.offset_seconds,
@@ -433,9 +433,11 @@ const _hasLooks = (t) => _LOOKS_WORD.test(String(t || ''));
@@ -433,9 +433,11 @@ const _hasLooks = (t) => _LOOKS_WORD.test(String(t || ''));
function _noLooksTags(tags) {
function _noLooksTags(tags) {
return (Array.isArray(tags) ? tags : []).filter((t) => typeof t === 'string' && t.trim() && !_LOOKS_TAG.test(t.trim()));
return (Array.isArray(tags) ? tags : []).filter((t) => typeof t === 'string' && t.trim() && !_LOOKS_TAG.test(t.trim()));
}
}
+// Body language is the person too ("reacting with a thoughtful pose", "in a relaxed posture"): the phrase goes.
+const _POSE = /,?\s*(?:(?:appearing|seeming|seemingly)\s+(?:to\s+be\s+)?)?(?:(?:reacting|sitting|standing|leaning|posing|looking|resting)\s+)?(?:with|in)\s+(?:an?|their|his|her)\s+(?:[\w-]+\s+){0,2}(?:pose|posture|expression|facial expression|stance)\b/gi;
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).replace(_POSE, '').replace(/\s+([,.;:!?])/g, '$1').replace(/ {2,}/g, ' ')
// Looks named: the person on camera. Only an age ("a child sits nearby"): someone, who may not be the streamer.
// Looks named: the person on camera. Only an age ("a child sits nearby"): someone, who may not be the streamer.
.replace(_LOOKED_AT, (m, art, looks, noun) => (looks ? _cap(m, 'the streamer') : _AGE_NOUN.test(noun) ? _cap(m, /^the$/i.test(art) ? 'the person' : 'someone') : m))
.replace(_LOOKED_AT, (m, art, looks, noun) => (looks ? _cap(m, 'the streamer') : _AGE_NOUN.test(noun) ? _cap(m, /^the$/i.test(art) ? 'the person' : 'someone') : m))
.replace(_LOOKED_AT_BARE, (m) => _cap(m, 'streamer'))
.replace(_LOOKED_AT_BARE, (m) => _cap(m, 'streamer'))
@@ -446,6 +448,16 @@ function _noLooks(text) {
@@ -446,6 +448,16 @@ function _noLooks(text) {
.replace(/\s*,\s*(?:shirtless|topless|bare-chested)\s*,/gi, '')
.replace(/\s*,\s*(?:shirtless|topless|bare-chested)\s*,/gi, '')
.replace(/\b(?:shirtless|topless|bare-chested)\b,?\s*/gi, '');
.replace(/\b(?:shirtless|topless|bare-chested)\b,?\s*/gi, '');
}
}
+/**
+ * A stream memory as the public sees it (home "Right now", a VOD's AI timeline, a streamer's AI profile): the
+ * observation only. The " — heard: "…"" clause is raw speech-to-text kept for the AI's own context (often another
+ * language, misheard, 500 characters); shown, it read as a wall of garbled text (2026-10-17, a Greek stream on the
+ * home page). Looks and pose wording go as everywhere public.
+ */
+function _publicMemoryText(text) {
+ const t = String(text || '').replace(/\s+—\s+heard:[\s\S]*$/, '').trim();
+ return (_noLooks(t) || '').trim();
+}
// Derive a concise short overview from a long one — the lead sentence(s), capped
// Derive a concise short overview from a long one — the lead sentence(s), capped
// ~150 chars at a sentence/word boundary. Deterministic + free (no AI call), so it
// ~150 chars at a sentence/word boundary. Deterministic + free (no AI call), so it
// can be cached at write time and shown on listing cards.
// can be cached at write time and shown on listing cards.
@@ -3586,7 +3598,7 @@ module.exports = {
@@ -3586,7 +3598,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, noLooksTags: _noLooksTags, hasLooks: _hasLooks, recordAiUsage, getAiCostToday, getAiCostTodayForUser, getAiUsageSummary,
+ cleanupMalformedAiText, noLooks: _noLooks, noLooksTags: _noLooksTags, hasLooks: _hasLooks, publicMemoryText: _publicMemoryText, 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,
@@ -273,7 +273,8 @@ router.get('/featured', async (req, res) => {
@@ -273,7 +273,8 @@ router.get('/featured', async (req, res) => {
let moment = null;
let moment = null;
try {
try {
const m = await db.getLatestStreamMemory(pick.id);
const m = await db.getLatestStreamMemory(pick.id);
- if (m && m.description) moment = { description: m.description, captured_at: m.captured_at || m.created_at || null, thumbnail_url: m.thumbnail_url || null };
+ const text = m && db.publicMemoryText(m.description);
+ if (text) moment = { description: text, captured_at: m.captured_at || m.created_at || null, thumbnail_url: m.thumbnail_url || null };
} catch { /* */ }
} catch { /* */ }
res.json({ stream, moment, count: live.length, index: idx, next_in_ms: FEATURED_ROTATE_MS - (Date.now() % FEATURED_ROTATE_MS) });
res.json({ stream, moment, count: live.length, index: idx, next_in_ms: FEATURED_ROTATE_MS - (Date.now() % FEATURED_ROTATE_MS) });
} catch (err) {
} catch (err) {
@@ -390,7 +390,7 @@ router.get('/:id/memories', optionalAuth, async (req, res) => {
@@ -390,7 +390,7 @@ router.get('/:id/memories', optionalAuth, async (req, res) => {
if (!streamId) return res.json({ memories: [] });
if (!streamId) return res.json({ memories: [] });
const memories = (await db.getStreamMemories(streamId) || []).map(m => ({
const memories = (await db.getStreamMemories(streamId) || []).map(m => ({
offset_seconds: m.offset_seconds,
offset_seconds: m.offset_seconds,
- description: m.description,
+ description: db.publicMemoryText(m.description),
tags: m.tags ? (() => { try { return JSON.parse(m.tags); } catch { return []; } })() : [],
tags: m.tags ? (() => { try { return JSON.parse(m.tags); } catch { return []; } })() : [],
}));
}));
res.json({ memories });
res.json({ memories });
@@ -27,6 +27,10 @@ const db = require('../server/db/database');
@@ -27,6 +27,10 @@ const db = require('../server/db/database');
['The view is close to the camera while wearing a black beanie and hoodie, with trees around.', 'The view is close to the camera, with trees around.'],
['The view is close to the camera while wearing a black beanie and hoodie, with trees around.', 'The view is close to the camera, with trees around.'],
['A man in a gray tank top sits against a plain light background.', 'A man sits against a plain light background.'],
['A man in a gray tank top sits against a plain light background.', 'A man sits against a plain light background.'],
['A man with a long beard and plaid shirt examines a woven item.', 'A man examines a woven item.'],
['A man with a long beard and plaid shirt examines a woven item.', 'A man examines a woven item.'],
+ // Pose and expression are the person too (2026-10-17, the home page's "Right now").
+ ['A live stream frame shows the streamer in a small window bottom-left, appearing to be reacting with a thoughtful pose while the main screen is black.',
+ 'A live stream frame shows the streamer in a small window bottom-left while the main screen is black.'],
+ ['The streamer sits in a relaxed posture at the desk.', 'The streamer sits at the desk.'],
['A kitchen studio with a headset-wearing host, microphone, and on-screen graphics.', 'A kitchen studio with a host, microphone, and on-screen graphics.'],
['A kitchen studio with a headset-wearing host, microphone, and on-screen graphics.', 'A kitchen studio with a host, microphone, and on-screen graphics.'],
['Panelists discuss while wearing headsets and microphones, with a trophy nearby.', 'Panelists discuss, with a trophy nearby.'],
['Panelists discuss while wearing headsets and microphones, with a trophy nearby.', 'Panelists discuss, with a trophy nearby.'],
['A man wearing sunglasses on his head looks down at the camera.', 'A man looks down at the camera.'],
['A man wearing sunglasses on his head looks down at the camera.', 'A man looks down at the camera.'],
@@ -46,6 +50,9 @@ const db = require('../server/db/database');
@@ -46,6 +50,9 @@ const db = require('../server/db/database');
['The streamer laughs — heard: "Put your headset on. I like your hair."', 'The streamer laughs — heard: "Put your headset on. I like your hair."'],
['The streamer laughs — heard: "Put your headset on. I like your hair."', 'The streamer laughs — heard: "Put your headset on. I like your hair."'],
];
];
let off = 0;
let off = 0;
+ // Public memory text is the observation alone: the raw transcript clause stays for the AI's context, never shown.
+ assert.strictEqual(db.publicMemoryText('The main screen is black, suggesting a pause. — heard: "Δεν θα πούμε άλλο. Τώρα που κερδίζουμε."'), 'The main screen is black, suggesting a pause.');
+ assert.strictEqual(db.publicMemoryText(null), '');
for (const [input, want] of cases) {
for (const [input, want] of cases) {
await db.addStreamMemory({ stream_id: sid, user_id: uid, offset_seconds: (off += 60), description: input });
await db.addStreamMemory({ stream_id: sid, user_id: uid, offset_seconds: (off += 60), description: input });
const row = await db.get('SELECT description FROM stream_memories WHERE stream_id = ? AND offset_seconds = ?', [sid, off]);
const row = await db.get('SELECT description FROM stream_memories WHERE stream_id = ? AND offset_seconds = ?', [sid, off]);