openvibers/openvibe.ai Commits
live.moments.pick: hide lines next to a used moment (#285)
Changes
@@@ -73,6 +73,8 @@ const MOMENT_FLAVOR = {paste: 'This moment becomes a SCREENSHOT paste: pick something visually striking in a single frame — a face or reaction, a visual gag, something odd on screen, a scene change. What was said matters less than what is SEEN.',clip: 'This moment becomes a 25-SECOND VIDEO CLIP: pick a beat that plays out over time — a line that lands, a reaction, a sound, chat exploding, something HAPPENING — not a static pretty frame.',};++// Live's moment registry keeps two moments of one VOD this far apart (server/ai/moment-registry.js GAP_SEC).++const MOMENT_GAP_SEC = 120;const ABSTAIN_CLIP = 'If NOTHING here is clearly worth a clip (only routine talk with no reaction, a static screen, nothing happening, or only moments already used), say so instead of forcing a pick: a missed clip costs less than a bad one. Then answer {"at": null, "title": "", "desc": "", "why": "<a few words>"}. Otherwise: ';const oneLine = (t, max) => String(t || '').replace(/\s+/g, ' ').trim().slice(0, max);/** "12:34", "[1:02:03]" or "75:12" → seconds; anything else (a bare number, words) → NaN. */@@@ -199,14 +201,23 @@ const PREPARE = {const times = (list, n) => ((list || []).length ? list.slice(0, n).map((t) => mmss(Number(t) || 0)).join(', ') : 'none');const avoid = (input.avoid || []).map(Number).filter((t) => Number.isFinite(t)).sort((a, b) => a - b);if (!(input.timeline || []).length && !(input.transcript || []).length) return { output: null };++ // What is within two minutes of a used moment is not shown at all. Told only to stay away, the model picked++ // 0:01 beside a used 0:06, and 4 of the 11-03 10:44 run's 12 picks were thrown away for it.++ const free = (t) => avoid.every((a) => Math.abs((Number(t) || 0) - a) >= MOMENT_GAP_SEC);++ const open = (list) => (list || []).filter((x) => free(x && x.t));++ const timeline = open(input.timeline);++ const transcript = open(input.transcript);++ if (!timeline.length && !transcript.length) {++ return { output: { t: null, title: '', desc: '', abstain: true, why: 'every moment shown is within two minutes of one already used' } };++ }return {vars: {title: oneLine(input.title, 80),-- timeline: lines(input.timeline, 180) || '(none)',-- transcript: lines(input.transcript, 160) || '(none)',-- sounds: lines(input.sounds, 80) || '(none)',-- clipped: times(input.clipped, 12),-- spikes: times(input.spikes, 6),++ timeline: lines(timeline, 180) || '(none)',++ transcript: lines(transcript, 160) || '(none)',++ sounds: lines(open(input.sounds), 80) || '(none)',++ clipped: times((input.clipped || []).filter(free), 12),++ spikes: times((input.spikes || []).filter(free), 6),flavor_hint: MOMENT_FLAVOR[input.flavor] || MOMENT_FLAVOR.paste,// Only a clip may come back empty (C09): a paste needs one good frame, and offering it the same way out// made the model decline all twelve VODs of the 11-03 run, leaving the home with no new pastes.@@@ -3,7 +3,8 @@// live.moments.pick: the prepare hook formats the timeline, transcript and sounds as [m:ss] lines, names clip and// chat signals, adds the flavor hint, and writes the "already used" line only when there is something to avoid;// nothing to work from is no run; the model answers with the moment's [m:ss] as written ("at"), since asked for--// seconds beside [m:ss] lines it wrote the minutes (11-03); the postprocess refuses an answer without a time.++// seconds beside [m:ss] lines it wrote the minutes (11-03); the postprocess refuses an answer without a time;++// lines within two minutes of a used moment are not shown (11-03: told only to avoid 0:06, it picked 0:01).const assert = require('assert');const { suite } = require('./helpers');const { PREPARE, POSTPROCESS } = require('../server/workflows/hooks');@@@ -15,7 +16,7 @@ const tpl = (live.templates || []).find((x) => x.key === 'live.moments.pick');t.test('the prompt renders as Live rendered it', () => {assert.ok(tpl, 'the template exists');-- const prep = PREPARE['live.moments.pick']({ title: 'Rust night', flavor: 'clip', timeline: [{ t: 95, text: 'streamer jumps up' }], transcript: [{ t: 94, text: 'IT COMPILES' }], sounds: [{ t: 96, label: 'Cheering' }], clipped: [95, 3605], spikes: [96], avoid: [300, 60] });++ const prep = PREPARE['live.moments.pick']({ title: 'Rust night', flavor: 'clip', timeline: [{ t: 95, text: 'streamer jumps up' }], transcript: [{ t: 94, text: 'IT COMPILES' }], sounds: [{ t: 96, label: 'Cheering' }], clipped: [95, 3605], spikes: [96], avoid: [300, 600] });const out = render(tpl.user_prompt, prep.vars);assert.match(out, /titled "Rust night"/);assert.match(out, /TIMELINE:\n\[1:35\] streamer jumps up/);@@@ -23,7 +24,7 @@ t.test('the prompt renders as Live rendered it', () => {assert.match(out, /\[1:36\] Cheering/);assert.match(out, /CLIPPED these timestamps .*: 1:35, 60:05/);assert.match(out, /25-SECOND VIDEO CLIP/);-- assert.match(out, /ALREADY USED .*: 1:00, 5:00\. Find a DIFFERENT moment\./);++ assert.match(out, /ALREADY USED .*: 5:00, 10:00\. Find a DIFFERENT moment\./);const none = render(tpl.user_prompt, PREPARE['live.moments.pick']({ title: 'x', timeline: [{ t: 1, text: 'a' }] }).vars);assert.ok(!/ALREADY USED/.test(none), 'no avoid line without anything to avoid');assert.match(none, /SCREENSHOT paste/, 'paste is the default flavor');@@@ -31,6 +32,24 @@ t.test('the prompt renders as Live rendered it', () => {assert.deepStrictEqual(PREPARE['live.moments.pick']({ title: 'x' }), { output: null }, 'nothing to work from: no run');});++t.test('what is next to a used moment is not shown (11-03: it picked 0:01 beside a used 0:06)', () => {++ const prep = PREPARE['live.moments.pick']({ title: 'x', flavor: 'paste',++ timeline: [{ t: 1, text: 'stream starts' }, { t: 125, text: 'still setting up' }, { t: 126, text: 'a cat walks in' }, { t: 560, text: 'big crash' }],++ transcript: [{ t: 4, text: 'hello' }, { t: 300, text: 'no way' }], sounds: [{ t: 7, label: 'Laughter' }, { t: 301, label: 'Gasp' }],++ clipped: [10, 900], spikes: [20, 302], avoid: [6, 470] });++ const out = render(tpl.user_prompt, prep.vars);++ assert.ok(!/stream starts|still setting up|hello|Laughter/.test(out), 'nothing within two minutes of 0:06');++ assert.ok(!/big crash/.test(out), 'nor of 7:50');++ assert.match(out, /TIMELINE:\n\[2:06\] a cat walks in\n\nTRANSCRIPT/, 'exactly two minutes away is free');++ assert.match(out, /TRANSCRIPT:\n\[5:00\] no way/);++ assert.match(out, /\[5:01\] Gasp/);++ assert.match(out, /CLIPPED these timestamps .*: 15:00\n/);++ assert.match(out, /SPIKED around: 5:02\n/);++ assert.match(out, /ALREADY USED .*: 0:06, 7:50\./, 'the used moments are still named');++ const none = PREPARE['live.moments.pick']({ title: 'x', flavor: 'clip', timeline: [{ t: 30, text: 'a' }], transcript: [{ t: 500, text: 'b' }], avoid: [60, 520] });++ assert.deepStrictEqual(none.output, { t: null, title: '', desc: '', abstain: true, why: 'every moment shown is within two minutes of one already used' }, 'nothing left to pick from: no run, an answer');++});++t.test('only a clip is offered the way out (C09): a paste always names its frame', () => {const clip = render(tpl.user_prompt, PREPARE['live.moments.pick']({ title: 'x', flavor: 'clip', timeline: [{ t: 1, text: 'a' }] }).vars);const paste = render(tpl.user_prompt, PREPARE['live.moments.pick']({ title: 'x', flavor: 'paste', timeline: [{ t: 1, text: 'a' }] }).vars);
@@ -73,6 +73,8 @@ const MOMENT_FLAVOR = {
@@ -73,6 +73,8 @@ const MOMENT_FLAVOR = {
paste: 'This moment becomes a SCREENSHOT paste: pick something visually striking in a single frame — a face or reaction, a visual gag, something odd on screen, a scene change. What was said matters less than what is SEEN.',
paste: 'This moment becomes a SCREENSHOT paste: pick something visually striking in a single frame — a face or reaction, a visual gag, something odd on screen, a scene change. What was said matters less than what is SEEN.',
clip: 'This moment becomes a 25-SECOND VIDEO CLIP: pick a beat that plays out over time — a line that lands, a reaction, a sound, chat exploding, something HAPPENING — not a static pretty frame.',
clip: 'This moment becomes a 25-SECOND VIDEO CLIP: pick a beat that plays out over time — a line that lands, a reaction, a sound, chat exploding, something HAPPENING — not a static pretty frame.',
};
};
+// Live's moment registry keeps two moments of one VOD this far apart (server/ai/moment-registry.js GAP_SEC).
+const MOMENT_GAP_SEC = 120;
const ABSTAIN_CLIP = 'If NOTHING here is clearly worth a clip (only routine talk with no reaction, a static screen, nothing happening, or only moments already used), say so instead of forcing a pick: a missed clip costs less than a bad one. Then answer {"at": null, "title": "", "desc": "", "why": "<a few words>"}. Otherwise: ';
const ABSTAIN_CLIP = 'If NOTHING here is clearly worth a clip (only routine talk with no reaction, a static screen, nothing happening, or only moments already used), say so instead of forcing a pick: a missed clip costs less than a bad one. Then answer {"at": null, "title": "", "desc": "", "why": "<a few words>"}. Otherwise: ';
const oneLine = (t, max) => String(t || '').replace(/\s+/g, ' ').trim().slice(0, max);
const oneLine = (t, max) => String(t || '').replace(/\s+/g, ' ').trim().slice(0, max);
/** "12:34", "[1:02:03]" or "75:12" → seconds; anything else (a bare number, words) → NaN. */
/** "12:34", "[1:02:03]" or "75:12" → seconds; anything else (a bare number, words) → NaN. */
@@ -199,14 +201,23 @@ const PREPARE = {
@@ -199,14 +201,23 @@ const PREPARE = {
const times = (list, n) => ((list || []).length ? list.slice(0, n).map((t) => mmss(Number(t) || 0)).join(', ') : 'none');
const times = (list, n) => ((list || []).length ? list.slice(0, n).map((t) => mmss(Number(t) || 0)).join(', ') : 'none');
const avoid = (input.avoid || []).map(Number).filter((t) => Number.isFinite(t)).sort((a, b) => a - b);
const avoid = (input.avoid || []).map(Number).filter((t) => Number.isFinite(t)).sort((a, b) => a - b);
if (!(input.timeline || []).length && !(input.transcript || []).length) return { output: null };
if (!(input.timeline || []).length && !(input.transcript || []).length) return { output: null };
+ // What is within two minutes of a used moment is not shown at all. Told only to stay away, the model picked
+ // 0:01 beside a used 0:06, and 4 of the 11-03 10:44 run's 12 picks were thrown away for it.
+ const free = (t) => avoid.every((a) => Math.abs((Number(t) || 0) - a) >= MOMENT_GAP_SEC);
+ const open = (list) => (list || []).filter((x) => free(x && x.t));
+ const timeline = open(input.timeline);
+ const transcript = open(input.transcript);
+ if (!timeline.length && !transcript.length) {
+ return { output: { t: null, title: '', desc: '', abstain: true, why: 'every moment shown is within two minutes of one already used' } };
+ }
return {
return {
vars: {
vars: {
title: oneLine(input.title, 80),
title: oneLine(input.title, 80),
- timeline: lines(input.timeline, 180) || '(none)',
- transcript: lines(input.transcript, 160) || '(none)',
- sounds: lines(input.sounds, 80) || '(none)',
- clipped: times(input.clipped, 12),
- spikes: times(input.spikes, 6),
+ timeline: lines(timeline, 180) || '(none)',
+ transcript: lines(transcript, 160) || '(none)',
+ sounds: lines(open(input.sounds), 80) || '(none)',
+ clipped: times((input.clipped || []).filter(free), 12),
+ spikes: times((input.spikes || []).filter(free), 6),
flavor_hint: MOMENT_FLAVOR[input.flavor] || MOMENT_FLAVOR.paste,
flavor_hint: MOMENT_FLAVOR[input.flavor] || MOMENT_FLAVOR.paste,
// Only a clip may come back empty (C09): a paste needs one good frame, and offering it the same way out
// Only a clip may come back empty (C09): a paste needs one good frame, and offering it the same way out
// made the model decline all twelve VODs of the 11-03 run, leaving the home with no new pastes.
// made the model decline all twelve VODs of the 11-03 run, leaving the home with no new pastes.
@@ -3,7 +3,8 @@
@@ -3,7 +3,8 @@
// live.moments.pick: the prepare hook formats the timeline, transcript and sounds as [m:ss] lines, names clip and
// live.moments.pick: the prepare hook formats the timeline, transcript and sounds as [m:ss] lines, names clip and
// chat signals, adds the flavor hint, and writes the "already used" line only when there is something to avoid;
// chat signals, adds the flavor hint, and writes the "already used" line only when there is something to avoid;
// nothing to work from is no run; the model answers with the moment's [m:ss] as written ("at"), since asked for
// nothing to work from is no run; the model answers with the moment's [m:ss] as written ("at"), since asked for
-// seconds beside [m:ss] lines it wrote the minutes (11-03); the postprocess refuses an answer without a time.
+// seconds beside [m:ss] lines it wrote the minutes (11-03); the postprocess refuses an answer without a time;
+// lines within two minutes of a used moment are not shown (11-03: told only to avoid 0:06, it picked 0:01).
const assert = require('assert');
const assert = require('assert');
const { suite } = require('./helpers');
const { suite } = require('./helpers');
const { PREPARE, POSTPROCESS } = require('../server/workflows/hooks');
const { PREPARE, POSTPROCESS } = require('../server/workflows/hooks');
@@ -15,7 +16,7 @@ const tpl = (live.templates || []).find((x) => x.key === 'live.moments.pick');
@@ -15,7 +16,7 @@ const tpl = (live.templates || []).find((x) => x.key === 'live.moments.pick');
t.test('the prompt renders as Live rendered it', () => {
t.test('the prompt renders as Live rendered it', () => {
assert.ok(tpl, 'the template exists');
assert.ok(tpl, 'the template exists');
- const prep = PREPARE['live.moments.pick']({ title: 'Rust night', flavor: 'clip', timeline: [{ t: 95, text: 'streamer jumps up' }], transcript: [{ t: 94, text: 'IT COMPILES' }], sounds: [{ t: 96, label: 'Cheering' }], clipped: [95, 3605], spikes: [96], avoid: [300, 60] });
+ const prep = PREPARE['live.moments.pick']({ title: 'Rust night', flavor: 'clip', timeline: [{ t: 95, text: 'streamer jumps up' }], transcript: [{ t: 94, text: 'IT COMPILES' }], sounds: [{ t: 96, label: 'Cheering' }], clipped: [95, 3605], spikes: [96], avoid: [300, 600] });
const out = render(tpl.user_prompt, prep.vars);
const out = render(tpl.user_prompt, prep.vars);
assert.match(out, /titled "Rust night"/);
assert.match(out, /titled "Rust night"/);
assert.match(out, /TIMELINE:\n\[1:35\] streamer jumps up/);
assert.match(out, /TIMELINE:\n\[1:35\] streamer jumps up/);
@@ -23,7 +24,7 @@ t.test('the prompt renders as Live rendered it', () => {
@@ -23,7 +24,7 @@ t.test('the prompt renders as Live rendered it', () => {
assert.match(out, /\[1:36\] Cheering/);
assert.match(out, /\[1:36\] Cheering/);
assert.match(out, /CLIPPED these timestamps .*: 1:35, 60:05/);
assert.match(out, /CLIPPED these timestamps .*: 1:35, 60:05/);
assert.match(out, /25-SECOND VIDEO CLIP/);
assert.match(out, /25-SECOND VIDEO CLIP/);
- assert.match(out, /ALREADY USED .*: 1:00, 5:00\. Find a DIFFERENT moment\./);
+ assert.match(out, /ALREADY USED .*: 5:00, 10:00\. Find a DIFFERENT moment\./);
const none = render(tpl.user_prompt, PREPARE['live.moments.pick']({ title: 'x', timeline: [{ t: 1, text: 'a' }] }).vars);
const none = render(tpl.user_prompt, PREPARE['live.moments.pick']({ title: 'x', timeline: [{ t: 1, text: 'a' }] }).vars);
assert.ok(!/ALREADY USED/.test(none), 'no avoid line without anything to avoid');
assert.ok(!/ALREADY USED/.test(none), 'no avoid line without anything to avoid');
assert.match(none, /SCREENSHOT paste/, 'paste is the default flavor');
assert.match(none, /SCREENSHOT paste/, 'paste is the default flavor');
@@ -31,6 +32,24 @@ t.test('the prompt renders as Live rendered it', () => {
@@ -31,6 +32,24 @@ t.test('the prompt renders as Live rendered it', () => {
assert.deepStrictEqual(PREPARE['live.moments.pick']({ title: 'x' }), { output: null }, 'nothing to work from: no run');
assert.deepStrictEqual(PREPARE['live.moments.pick']({ title: 'x' }), { output: null }, 'nothing to work from: no run');
});
});
+t.test('what is next to a used moment is not shown (11-03: it picked 0:01 beside a used 0:06)', () => {
+ const prep = PREPARE['live.moments.pick']({ title: 'x', flavor: 'paste',
+ timeline: [{ t: 1, text: 'stream starts' }, { t: 125, text: 'still setting up' }, { t: 126, text: 'a cat walks in' }, { t: 560, text: 'big crash' }],
+ transcript: [{ t: 4, text: 'hello' }, { t: 300, text: 'no way' }], sounds: [{ t: 7, label: 'Laughter' }, { t: 301, label: 'Gasp' }],
+ clipped: [10, 900], spikes: [20, 302], avoid: [6, 470] });
+ const out = render(tpl.user_prompt, prep.vars);
+ assert.ok(!/stream starts|still setting up|hello|Laughter/.test(out), 'nothing within two minutes of 0:06');
+ assert.ok(!/big crash/.test(out), 'nor of 7:50');
+ assert.match(out, /TIMELINE:\n\[2:06\] a cat walks in\n\nTRANSCRIPT/, 'exactly two minutes away is free');
+ assert.match(out, /TRANSCRIPT:\n\[5:00\] no way/);
+ assert.match(out, /\[5:01\] Gasp/);
+ assert.match(out, /CLIPPED these timestamps .*: 15:00\n/);
+ assert.match(out, /SPIKED around: 5:02\n/);
+ assert.match(out, /ALREADY USED .*: 0:06, 7:50\./, 'the used moments are still named');
+ const none = PREPARE['live.moments.pick']({ title: 'x', flavor: 'clip', timeline: [{ t: 30, text: 'a' }], transcript: [{ t: 500, text: 'b' }], avoid: [60, 520] });
+ assert.deepStrictEqual(none.output, { t: null, title: '', desc: '', abstain: true, why: 'every moment shown is within two minutes of one already used' }, 'nothing left to pick from: no run, an answer');
+});
+
t.test('only a clip is offered the way out (C09): a paste always names its frame', () => {
t.test('only a clip is offered the way out (C09): a paste always names its frame', () => {
const clip = render(tpl.user_prompt, PREPARE['live.moments.pick']({ title: 'x', flavor: 'clip', timeline: [{ t: 1, text: 'a' }] }).vars);
const clip = render(tpl.user_prompt, PREPARE['live.moments.pick']({ title: 'x', flavor: 'clip', timeline: [{ t: 1, text: 'a' }] }).vars);
const paste = render(tpl.user_prompt, PREPARE['live.moments.pick']({ title: 'x', flavor: 'paste', timeline: [{ t: 1, text: 'a' }] }).vars);
const paste = render(tpl.user_prompt, PREPARE['live.moments.pick']({ title: 'x', flavor: 'paste', timeline: [{ t: 1, text: 'a' }] }).vars);