openvibers/openvibe.live Commits
Recently Online cards carry the AI's own line when it reads as a person wrote it (#811)
Changes
@@@ -126,6 +126,8 @@ async function generateStreamerOverview(userId) {try {await db.upsertStreamerOverview(userId, {overview,++ // The card's own line (OpenVibe.AI #466); kept only when it passes channel-line.js humanLine.++ line: o && typeof o.line === 'string' ? o.line.slice(0, 140) : null,model: 'openvibe-ai',sources: JSON.stringify({ memories: memories.length, vods: vods.length, pastes: summarized.length }),});@@@ -174,4 +174,33 @@ function shapeChannelLine({ username = null, displayName = null, overview = '' }return FALLBACK_CHANNEL_LINE;}--module.exports = { shapeChannelLine, CHANNEL_LINE_RE, FALLBACK_CHANNEL_LINE, MAX_LEN };++/*++ * The AI's own card line (2026-11-11, ask 363: the owner, "not all so generic sounding … more human feeling and nice").++ * OpenVibe.AI's live.streamer.overview answers a `line` written for the card ("Robots, cameras and late-night++ * tinkering, with chat riding along."); it is kept when it is one sentence of 20 to 100 characters, without the name, a++ * colon or a handle, without the stock words that make every channel sound the same, not explicit, and not in the++ * reshaped "Streams …" shape (that shape is the fallback's, so the reshape pass knows its own lines). Otherwise null,++ * and the card falls back to the overview reshaped.++ */++const STOCK = /\b(?:focus|content|engaging|various|a mix of|dive into|diving into|showcas\w*|vibrant|dynamic|journey|immersive|a blend of)\b/i;++function humanLine(raw, { username = null, displayName = null } = {}) {++ let s = plain(raw).replace(/^["'\u201c\u201d]+|["'\u201c\u201d]+$/g, '').trim();++ if (!s) return null;++ if (!/[.!?\u2026]$/.test(s)) s += '.';++ if (s.length < 20 || s.length > 100) return null;++ if (/^Streams\b/.test(s) || s.includes(':') || /@\w/.test(s)) return null;++ if (sentences(s).length > 1) return null;++ for (const n of [displayName, username]) {++ const name = String(n || '').trim();++ if (name && new RegExp(`^${escRe(name)}\\b`, 'i').test(s)) return null;++ }++ if (STOCK.test(s) || isExplicit(s)) return null;++ return s.charAt(0).toUpperCase() + s.slice(1);++}++++/** The card's line: the AI's own when it passes, else the overview in the one shape. */++function cardLine({ username = null, displayName = null, overview = '', line = null } = {}) {++ return humanLine(line, { username, displayName }) || shapeChannelLine({ username, displayName, overview });++}++++module.exports = { shapeChannelLine, humanLine, cardLine, CHANNEL_LINE_RE, FALLBACK_CHANNEL_LINE, MAX_LEN };@@@ -464,14 +464,14 @@ function _shortOverview(text) {return (sp > 40 ? cut.slice(0, sp) : cut).trim() + '…';}/** A streamer's card line in the one channel shape (server/ai/channel-line.js); the name comes off via their row. */--async function _shapedChannelLine(userId, overview) {++async function _shapedChannelLine(userId, overview, line = null) {let username = null, displayName = null;try {const u = await get('SELECT username, display_name FROM users WHERE id = ?', [userId]);if (u) { username = u.username; displayName = u.display_name; }} catch { /* without the name the line still shapes, minus the name strip */ }try {-- return require('../ai/channel-line').shapeChannelLine({ username, displayName, overview: _plainProse(overview || '') });++ return require('../ai/channel-line').cardLine({ username, displayName, overview: _plainProse(overview || ''), line });} catch {return _shortOverview(overview);}@@@ -928,10 +928,10 @@ async function getStreamTranscriptSegments(streamId) {// countAutoClipsSince() removed — the media subsystem (vods/clips/pastes writes) moved to OpenVibe.Media.--async function upsertStreamerOverview(userId, { overview, model = null, sources = null }) {-- // The card line is stored in the one channel shape, so the streamer-overview job refreshes stored lines-- // through this write; rows it has not rewritten yet are shaped on read (GET /api/streams/recently-online).-- const short = await _shapedChannelLine(userId, overview);++async function upsertStreamerOverview(userId, { overview, line = null, model = null, sources = null }) {++ // The card line: the AI's own line when it passes (server/ai/channel-line.js humanLine, ask 363), else the overview++ // in the one channel shape; rows the job has not rewritten yet are shaped on read (GET /api/streams/recently-online).++ const short = await _shapedChannelLine(userId, overview, line);return await run(`INSERT INTO streamer_overviews (user_id, overview, overview_short, model, sources, generated_at)VALUES (?, ?, ?, ?, ?, ov_now())ON CONFLICT(user_id) DO UPDATE SET@@@ -955,8 +955,11 @@ async function shapeStoredStreamerOverviewShorts(limit = 20) {FROM streamer_overviews o LEFT JOIN users u ON u.id = o.user_idWHERE o.overview IS NOT NULL AND TRIM(o.overview) <> ''ORDER BY o.generated_at ASC NULLS FIRST LIMIT ?`, [limit]);++ const { humanLine } = require('../ai/channel-line');let changed = 0;for (const r of rows) {++ // The AI's own card line stays (it is never in the reshaped "Streams …" shape).++ if (humanLine(r.overview_short, { username: r.username, displayName: r.display_name })) continue;const short = await _shapedChannelLine(r.user_id, r.overview);if (short && short !== r.overview_short) {await run('UPDATE streamer_overviews SET overview_short = ? WHERE user_id = ?', [short, r.user_id]);@@@ -1452,11 +1452,13 @@ router.get('/recently-online', async (req, res) => {}// Every card carries the same numbers, present or null, and its line in the one channel shape// (stored lines the job has not rewritten yet are shaped here).-- const { shapeChannelLine } = require('../ai/channel-line');++ const { shapeChannelLine, humanLine } = require('../ai/channel-line');for (const s of streamers) {if (!s.recent_facts) { s.recent_facts = { streams: [] }; for (const k of RECENT_FACT_KEYS) s.recent_facts[k] = null; }try {-- s.ai_overview_short = shapeChannelLine({ username: s.username, displayName: s.display_name, overview: s.ai_overview || s.ai_overview_short });++ // The AI's own line when it is stored (ask 363), else the overview in the one shape.++ const names = { username: s.username, displayName: s.display_name };++ s.ai_overview_short = humanLine(s.ai_overview_short, names) || shapeChannelLine({ ...names, overview: s.ai_overview || s.ai_overview_short });} catch { /* the stored line stands */ }}res.json({ streamers, total, limit, offset, hasMore: offset + streamers.length < total });@@@ -116,4 +116,19 @@ assert.match(FALLBACK_CHANNEL_LINE, CHANNEL_LINE_RE, 'the fallback is the same sassert.strictEqual(shapeChannelLine(), FALLBACK_CHANNEL_LINE, 'no arguments');assert.strictEqual(shapeChannelLine({ overview: null }), FALLBACK_CHANNEL_LINE, 'null overview');assert.strictEqual(shapeChannelLine({ username: 'Maticus', displayName: 'Maticus', overview: '{"overview":"Maticus streams chess and cooking."}' }), 'Streams chess and cooking.', 'JSON-wrapped model text');++// The AI's own card line (ask 363, 2026-11-11): kept when it reads as a person wrote it, else the shape above.++{++ const { humanLine, cardLine } = require('../server/ai/channel-line');++ const names = { username: 'Maticus', displayName: 'Maticus' };++ assert.strictEqual(humanLine('Robots, cameras and late-night tinkering, with chat riding along.', names), 'Robots, cameras and late-night tinkering, with chat riding along.');++ assert.strictEqual(humanLine('cozy acoustic sets and requests from a kitchen table', names), 'Cozy acoustic sets and requests from a kitchen table.', 'a capital and a full stop');++ assert.strictEqual(humanLine('Maticus builds robots in his kitchen.', names), null, 'the card shows the name already');++ assert.strictEqual(humanLine('Streams robots and cameras.', names), null, 'the reshaped shape is the fallback\'s own');++ assert.strictEqual(humanLine('A vibrant mix of tech content.', names), null, 'the stock words');++ assert.strictEqual(humanLine('Chess. Then cooking for everyone.', names), null, 'one sentence');++ assert.strictEqual(humanLine('Robots: built, broken, built again.', names), null, 'no colon');++ assert.strictEqual(humanLine('Short.', names), null, 'too short to say anything');++ assert.strictEqual(cardLine({ ...names, overview: 'Maticus streams chess and cooking.', line: 'A dynamic journey.' }), 'Streams chess and cooking.', 'a stock line falls back to the shape');++ assert.strictEqual(cardLine({ ...names, overview: 'Maticus streams chess.', line: 'Chess openings argued over with chat, one blunder at a time.' }), 'Chess openings argued over with chat, one blunder at a time.');++}console.log('channel-line shape: all checks passed');
@@ -126,6 +126,8 @@ async function generateStreamerOverview(userId) {
@@ -126,6 +126,8 @@ async function generateStreamerOverview(userId) {
try {
try {
await db.upsertStreamerOverview(userId, {
await db.upsertStreamerOverview(userId, {
overview,
overview,
+ // The card's own line (OpenVibe.AI #466); kept only when it passes channel-line.js humanLine.
+ line: o && typeof o.line === 'string' ? o.line.slice(0, 140) : null,
model: 'openvibe-ai',
model: 'openvibe-ai',
sources: JSON.stringify({ memories: memories.length, vods: vods.length, pastes: summarized.length }),
sources: JSON.stringify({ memories: memories.length, vods: vods.length, pastes: summarized.length }),
});
});
@@ -174,4 +174,33 @@ function shapeChannelLine({ username = null, displayName = null, overview = '' }
@@ -174,4 +174,33 @@ function shapeChannelLine({ username = null, displayName = null, overview = '' }
return FALLBACK_CHANNEL_LINE;
return FALLBACK_CHANNEL_LINE;
}
}
-module.exports = { shapeChannelLine, CHANNEL_LINE_RE, FALLBACK_CHANNEL_LINE, MAX_LEN };
+/*
+ * The AI's own card line (2026-11-11, ask 363: the owner, "not all so generic sounding … more human feeling and nice").
+ * OpenVibe.AI's live.streamer.overview answers a `line` written for the card ("Robots, cameras and late-night
+ * tinkering, with chat riding along."); it is kept when it is one sentence of 20 to 100 characters, without the name, a
+ * colon or a handle, without the stock words that make every channel sound the same, not explicit, and not in the
+ * reshaped "Streams …" shape (that shape is the fallback's, so the reshape pass knows its own lines). Otherwise null,
+ * and the card falls back to the overview reshaped.
+ */
+const STOCK = /\b(?:focus|content|engaging|various|a mix of|dive into|diving into|showcas\w*|vibrant|dynamic|journey|immersive|a blend of)\b/i;
+function humanLine(raw, { username = null, displayName = null } = {}) {
+ let s = plain(raw).replace(/^["'\u201c\u201d]+|["'\u201c\u201d]+$/g, '').trim();
+ if (!s) return null;
+ if (!/[.!?\u2026]$/.test(s)) s += '.';
+ if (s.length < 20 || s.length > 100) return null;
+ if (/^Streams\b/.test(s) || s.includes(':') || /@\w/.test(s)) return null;
+ if (sentences(s).length > 1) return null;
+ for (const n of [displayName, username]) {
+ const name = String(n || '').trim();
+ if (name && new RegExp(`^${escRe(name)}\\b`, 'i').test(s)) return null;
+ }
+ if (STOCK.test(s) || isExplicit(s)) return null;
+ return s.charAt(0).toUpperCase() + s.slice(1);
+}
+
+/** The card's line: the AI's own when it passes, else the overview in the one shape. */
+function cardLine({ username = null, displayName = null, overview = '', line = null } = {}) {
+ return humanLine(line, { username, displayName }) || shapeChannelLine({ username, displayName, overview });
+}
+
+module.exports = { shapeChannelLine, humanLine, cardLine, CHANNEL_LINE_RE, FALLBACK_CHANNEL_LINE, MAX_LEN };
@@ -464,14 +464,14 @@ function _shortOverview(text) {
@@ -464,14 +464,14 @@ function _shortOverview(text) {
return (sp > 40 ? cut.slice(0, sp) : cut).trim() + '…';
return (sp > 40 ? cut.slice(0, sp) : cut).trim() + '…';
}
}
/** A streamer's card line in the one channel shape (server/ai/channel-line.js); the name comes off via their row. */
/** A streamer's card line in the one channel shape (server/ai/channel-line.js); the name comes off via their row. */
-async function _shapedChannelLine(userId, overview) {
+async function _shapedChannelLine(userId, overview, line = null) {
let username = null, displayName = null;
let username = null, displayName = null;
try {
try {
const u = await get('SELECT username, display_name FROM users WHERE id = ?', [userId]);
const u = await get('SELECT username, display_name FROM users WHERE id = ?', [userId]);
if (u) { username = u.username; displayName = u.display_name; }
if (u) { username = u.username; displayName = u.display_name; }
} catch { /* without the name the line still shapes, minus the name strip */ }
} catch { /* without the name the line still shapes, minus the name strip */ }
try {
try {
- return require('../ai/channel-line').shapeChannelLine({ username, displayName, overview: _plainProse(overview || '') });
+ return require('../ai/channel-line').cardLine({ username, displayName, overview: _plainProse(overview || ''), line });
} catch {
} catch {
return _shortOverview(overview);
return _shortOverview(overview);
}
}
@@ -928,10 +928,10 @@ async function getStreamTranscriptSegments(streamId) {
@@ -928,10 +928,10 @@ async function getStreamTranscriptSegments(streamId) {
// countAutoClipsSince() removed — the media subsystem (vods/clips/pastes writes) moved to OpenVibe.Media.
// countAutoClipsSince() removed — the media subsystem (vods/clips/pastes writes) moved to OpenVibe.Media.
-async function upsertStreamerOverview(userId, { overview, model = null, sources = null }) {
- // The card line is stored in the one channel shape, so the streamer-overview job refreshes stored lines
- // through this write; rows it has not rewritten yet are shaped on read (GET /api/streams/recently-online).
- const short = await _shapedChannelLine(userId, overview);
+async function upsertStreamerOverview(userId, { overview, line = null, model = null, sources = null }) {
+ // The card line: the AI's own line when it passes (server/ai/channel-line.js humanLine, ask 363), else the overview
+ // in the one channel shape; rows the job has not rewritten yet are shaped on read (GET /api/streams/recently-online).
+ const short = await _shapedChannelLine(userId, overview, line);
return await run(`INSERT INTO streamer_overviews (user_id, overview, overview_short, model, sources, generated_at)
return await run(`INSERT INTO streamer_overviews (user_id, overview, overview_short, model, sources, generated_at)
VALUES (?, ?, ?, ?, ?, ov_now())
VALUES (?, ?, ?, ?, ?, ov_now())
ON CONFLICT(user_id) DO UPDATE SET
ON CONFLICT(user_id) DO UPDATE SET
@@ -955,8 +955,11 @@ async function shapeStoredStreamerOverviewShorts(limit = 20) {
@@ -955,8 +955,11 @@ async function shapeStoredStreamerOverviewShorts(limit = 20) {
FROM streamer_overviews o LEFT JOIN users u ON u.id = o.user_id
FROM streamer_overviews o LEFT JOIN users u ON u.id = o.user_id
WHERE o.overview IS NOT NULL AND TRIM(o.overview) <> ''
WHERE o.overview IS NOT NULL AND TRIM(o.overview) <> ''
ORDER BY o.generated_at ASC NULLS FIRST LIMIT ?`, [limit]);
ORDER BY o.generated_at ASC NULLS FIRST LIMIT ?`, [limit]);
+ const { humanLine } = require('../ai/channel-line');
let changed = 0;
let changed = 0;
for (const r of rows) {
for (const r of rows) {
+ // The AI's own card line stays (it is never in the reshaped "Streams …" shape).
+ if (humanLine(r.overview_short, { username: r.username, displayName: r.display_name })) continue;
const short = await _shapedChannelLine(r.user_id, r.overview);
const short = await _shapedChannelLine(r.user_id, r.overview);
if (short && short !== r.overview_short) {
if (short && short !== r.overview_short) {
await run('UPDATE streamer_overviews SET overview_short = ? WHERE user_id = ?', [short, r.user_id]);
await run('UPDATE streamer_overviews SET overview_short = ? WHERE user_id = ?', [short, r.user_id]);
@@ -1452,11 +1452,13 @@ router.get('/recently-online', async (req, res) => {
@@ -1452,11 +1452,13 @@ router.get('/recently-online', async (req, res) => {
}
}
// Every card carries the same numbers, present or null, and its line in the one channel shape
// Every card carries the same numbers, present or null, and its line in the one channel shape
// (stored lines the job has not rewritten yet are shaped here).
// (stored lines the job has not rewritten yet are shaped here).
- const { shapeChannelLine } = require('../ai/channel-line');
+ const { shapeChannelLine, humanLine } = require('../ai/channel-line');
for (const s of streamers) {
for (const s of streamers) {
if (!s.recent_facts) { s.recent_facts = { streams: [] }; for (const k of RECENT_FACT_KEYS) s.recent_facts[k] = null; }
if (!s.recent_facts) { s.recent_facts = { streams: [] }; for (const k of RECENT_FACT_KEYS) s.recent_facts[k] = null; }
try {
try {
- s.ai_overview_short = shapeChannelLine({ username: s.username, displayName: s.display_name, overview: s.ai_overview || s.ai_overview_short });
+ // The AI's own line when it is stored (ask 363), else the overview in the one shape.
+ const names = { username: s.username, displayName: s.display_name };
+ s.ai_overview_short = humanLine(s.ai_overview_short, names) || shapeChannelLine({ ...names, overview: s.ai_overview || s.ai_overview_short });
} catch { /* the stored line stands */ }
} catch { /* the stored line stands */ }
}
}
res.json({ streamers, total, limit, offset, hasMore: offset + streamers.length < total });
res.json({ streamers, total, limit, offset, hasMore: offset + streamers.length < total });
@@ -116,4 +116,19 @@ assert.match(FALLBACK_CHANNEL_LINE, CHANNEL_LINE_RE, 'the fallback is the same s
@@ -116,4 +116,19 @@ assert.match(FALLBACK_CHANNEL_LINE, CHANNEL_LINE_RE, 'the fallback is the same s
assert.strictEqual(shapeChannelLine(), FALLBACK_CHANNEL_LINE, 'no arguments');
assert.strictEqual(shapeChannelLine(), FALLBACK_CHANNEL_LINE, 'no arguments');
assert.strictEqual(shapeChannelLine({ overview: null }), FALLBACK_CHANNEL_LINE, 'null overview');
assert.strictEqual(shapeChannelLine({ overview: null }), FALLBACK_CHANNEL_LINE, 'null overview');
assert.strictEqual(shapeChannelLine({ username: 'Maticus', displayName: 'Maticus', overview: '{"overview":"Maticus streams chess and cooking."}' }), 'Streams chess and cooking.', 'JSON-wrapped model text');
assert.strictEqual(shapeChannelLine({ username: 'Maticus', displayName: 'Maticus', overview: '{"overview":"Maticus streams chess and cooking."}' }), 'Streams chess and cooking.', 'JSON-wrapped model text');
+// The AI's own card line (ask 363, 2026-11-11): kept when it reads as a person wrote it, else the shape above.
+{
+ const { humanLine, cardLine } = require('../server/ai/channel-line');
+ const names = { username: 'Maticus', displayName: 'Maticus' };
+ assert.strictEqual(humanLine('Robots, cameras and late-night tinkering, with chat riding along.', names), 'Robots, cameras and late-night tinkering, with chat riding along.');
+ assert.strictEqual(humanLine('cozy acoustic sets and requests from a kitchen table', names), 'Cozy acoustic sets and requests from a kitchen table.', 'a capital and a full stop');
+ assert.strictEqual(humanLine('Maticus builds robots in his kitchen.', names), null, 'the card shows the name already');
+ assert.strictEqual(humanLine('Streams robots and cameras.', names), null, 'the reshaped shape is the fallback\'s own');
+ assert.strictEqual(humanLine('A vibrant mix of tech content.', names), null, 'the stock words');
+ assert.strictEqual(humanLine('Chess. Then cooking for everyone.', names), null, 'one sentence');
+ assert.strictEqual(humanLine('Robots: built, broken, built again.', names), null, 'no colon');
+ assert.strictEqual(humanLine('Short.', names), null, 'too short to say anything');
+ assert.strictEqual(cardLine({ ...names, overview: 'Maticus streams chess and cooking.', line: 'A dynamic journey.' }), 'Streams chess and cooking.', 'a stock line falls back to the shape');
+ assert.strictEqual(cardLine({ ...names, overview: 'Maticus streams chess.', line: 'Chess openings argued over with chat, one blunder at a time.' }), 'Chess openings argued over with chat, one blunder at a time.');
+}
console.log('channel-line shape: all checks passed');
console.log('channel-line shape: all checks passed');