openvibers/openvibe.chat Commits
The timeline counts people only: Live's AI viewers make no spike (item 151, C07) (#338)
Changes
@@@ -210,6 +210,8 @@ async function dmBlockState(req) {}// ── N3: timeline ──++// People's chat only: Live's AI viewers (source_platform 'ai') are left out, so their chatter never makes a "spike" the++// AI then clips (item 151, C07: AI clips are excluded from human signals).async function timeline(req) {const q = query(req, { channel_user_id: 'id', stream_id: 'id', since: 'ms', until: 'ms', bucket_ms: 'id' });if ((q.channel_user_id == null) === (q.stream_id == null)) bad('Give channel_user_id or stream_id');@@@ -223,7 +225,8 @@ async function timeline(req) {`SELECT n, COUNT(*) AS count, MAX(id) AS max_id FROM (SELECT id, floor((extract(epoch FROM ov_ts(timestamp)) * 1000 - ?) / ?)::bigint AS nFROM chat_messages-- WHERE ${col} = ? AND is_deleted = 0 AND timestamp >= ? AND timestamp < ?) m++ WHERE ${col} = ? AND is_deleted = 0 AND timestamp >= ? AND timestamp < ?++ AND COALESCE(source_platform, '') <> 'ai') mWHERE n IS NOT NULL AND n >= 0 GROUP BY n ORDER BY n`,[q.since, q.bucket_ms, q[col], sqlTime(q.since), sqlTime(until)]);return {@@@ -204,6 +204,16 @@ t('N3 timeline: buckets from since, oldest first, empty ones left out', async ()check('chat.timeline-result@1', await tl(q, 400));});++t('N3 timeline: Live\'s AI viewers (source_platform ai) are not chat activity', async () => {++ const tl = (q) => read(`/timeline?${q}`, 'chat.analysis.read', 'chat.timeline-result@1');++ const before = await tl(`stream_id=${streamId}&since=${T}&until=${T + 600e3}&bucket_ms=60000`);++ await line('ai1', { ms: T + 70e3, username: 'Sora (AI)', platform: 'ai' });++ await line('ai2', { ms: T + 75e3, username: 'Kai (AI)', platform: 'ai' });++ const after = await tl(`stream_id=${streamId}&since=${T}&until=${T + 600e3}&bucket_ms=60000`);++ assert.deepStrictEqual(after.buckets, before.buckets, 'the AI\'s chatter makes no spike');++ await h.db.run('DELETE FROM chat_messages WHERE id = ANY(?::bigint[])', [[ids.ai1, ids.ai2]]);++});++t('N1c first chat: whether this identity has ever chatted in the channel', async () => {await h.db.run(`INSERT INTO stream_first_chats (chatter_key, channel_user_id) VALUES ('user:123', ?), ('anon:anonX', ?), ('ext:[Twitch] foo', ?)`,[streamer.id, streamer.id, streamer.id]);
@@ -210,6 +210,8 @@ async function dmBlockState(req) {
@@ -210,6 +210,8 @@ async function dmBlockState(req) {
}
}
// ── N3: timeline ──
// ── N3: timeline ──
+// People's chat only: Live's AI viewers (source_platform 'ai') are left out, so their chatter never makes a "spike" the
+// AI then clips (item 151, C07: AI clips are excluded from human signals).
async function timeline(req) {
async function timeline(req) {
const q = query(req, { channel_user_id: 'id', stream_id: 'id', since: 'ms', until: 'ms', bucket_ms: 'id' });
const q = query(req, { channel_user_id: 'id', stream_id: 'id', since: 'ms', until: 'ms', bucket_ms: 'id' });
if ((q.channel_user_id == null) === (q.stream_id == null)) bad('Give channel_user_id or stream_id');
if ((q.channel_user_id == null) === (q.stream_id == null)) bad('Give channel_user_id or stream_id');
@@ -223,7 +225,8 @@ async function timeline(req) {
@@ -223,7 +225,8 @@ async function timeline(req) {
`SELECT n, COUNT(*) AS count, MAX(id) AS max_id FROM (
`SELECT n, COUNT(*) AS count, MAX(id) AS max_id FROM (
SELECT id, floor((extract(epoch FROM ov_ts(timestamp)) * 1000 - ?) / ?)::bigint AS n
SELECT id, floor((extract(epoch FROM ov_ts(timestamp)) * 1000 - ?) / ?)::bigint AS n
FROM chat_messages
FROM chat_messages
- WHERE ${col} = ? AND is_deleted = 0 AND timestamp >= ? AND timestamp < ?) m
+ WHERE ${col} = ? AND is_deleted = 0 AND timestamp >= ? AND timestamp < ?
+ AND COALESCE(source_platform, '') <> 'ai') m
WHERE n IS NOT NULL AND n >= 0 GROUP BY n ORDER BY n`,
WHERE n IS NOT NULL AND n >= 0 GROUP BY n ORDER BY n`,
[q.since, q.bucket_ms, q[col], sqlTime(q.since), sqlTime(until)]);
[q.since, q.bucket_ms, q[col], sqlTime(q.since), sqlTime(until)]);
return {
return {
@@ -204,6 +204,16 @@ t('N3 timeline: buckets from since, oldest first, empty ones left out', async ()
@@ -204,6 +204,16 @@ t('N3 timeline: buckets from since, oldest first, empty ones left out', async ()
check('chat.timeline-result@1', await tl(q, 400));
check('chat.timeline-result@1', await tl(q, 400));
});
});
+t('N3 timeline: Live\'s AI viewers (source_platform ai) are not chat activity', async () => {
+ const tl = (q) => read(`/timeline?${q}`, 'chat.analysis.read', 'chat.timeline-result@1');
+ const before = await tl(`stream_id=${streamId}&since=${T}&until=${T + 600e3}&bucket_ms=60000`);
+ await line('ai1', { ms: T + 70e3, username: 'Sora (AI)', platform: 'ai' });
+ await line('ai2', { ms: T + 75e3, username: 'Kai (AI)', platform: 'ai' });
+ const after = await tl(`stream_id=${streamId}&since=${T}&until=${T + 600e3}&bucket_ms=60000`);
+ assert.deepStrictEqual(after.buckets, before.buckets, 'the AI\'s chatter makes no spike');
+ await h.db.run('DELETE FROM chat_messages WHERE id = ANY(?::bigint[])', [[ids.ai1, ids.ai2]]);
+});
+
t('N1c first chat: whether this identity has ever chatted in the channel', async () => {
t('N1c first chat: whether this identity has ever chatted in the channel', async () => {
await h.db.run(`INSERT INTO stream_first_chats (chatter_key, channel_user_id) VALUES ('user:123', ?), ('anon:anonX', ?), ('ext:[Twitch] foo', ?)`,
await h.db.run(`INSERT INTO stream_first_chats (chatter_key, channel_user_id) VALUES ('user:123', ?), ('anon:anonX', ?), ('ext:[Twitch] foo', ?)`,
[streamer.id, streamer.id, streamer.id]);
[streamer.id, streamer.id, streamer.id]);