openvibers/openvibe.live Commits

A declined paste falls back to the signals; only a clip may come back empty (#588)

OP OpenVibers committed 10 days ago · c55e2133f7 · 2 files, +11 −4

After AI #273 the moments job's next run had the AI decline all twelve VODs, and the home got no new pastes. A clip's
abstention stays final (C08/C09); a paste needs one good frame, so when the AI declines one the objective signals pick
it, as they do when the AI is off. AI #277 stops offering pastes the way out at all; this is the safety net.

Changes

ddiff --git a/server/ai/ai-moments-job.js b/server/ai/ai-moments-job.js
iindex c190582e..6919ff6d 100644
---- a/server/ai/ai-moments-job.js
++++ b/server/ai/ai-moments-job.js
@@@ -264,11 +264,13 @@ async function _findBestMoment(vod, { flavor = 'paste', avoid = [] } = {}) {
};
try {
const j = await aiService.structured('live.moments.pick', input, { target: { service: 'live', type: 'vod', id: String(vod.vod_id) }, meter: { kind: 'moment_pick', role: 'legacy' } });
-- // The AI may say nothing here is worth it (C08/C09): that is an answer, not a failure, so no weaker signal
-- // is tried in its place.
++ // The AI may say nothing here is worth a clip (C08/C09): that is an answer, not a failure, so no weaker
++ // signal is tried in its place. A paste needs only one good frame: if the AI declines one anyway, the
++ // objective signals below pick it (11-03: twelve declined pastes left the home with none).
if (j && (j.abstain === true || (Object.prototype.hasOwnProperty.call(j, 't') && j.t === null))) {
-- console.log(`[AI-Moments] VOD ${vod.vod_id}: the AI found nothing worth a ${flavor === 'clip' ? 'clip' : 'paste'}${j.why ? ` (${_cleanText(j.why, 120)})` : ''}`);
-- return null;
++ console.log(`[AI-Moments] VOD ${vod.vod_id}: the AI found nothing worth a ${flavor === 'clip' ? 'clip' : 'paste'}${j.why ? ` (${_cleanText(j.why, 120)})` : ''}${flavor === 'clip' ? '' : ': the signals pick the frame'}`);
++ if (flavor === 'clip') return null;
++ throw new Error('declined paste');
}
let t = j ? Number(j.t) : NaN;
if (!isNaN(t)) {
ddiff --git a/test/vod-moment-evidence.test.js b/test/vod-moment-evidence.test.js
iindex 0515c00a..f92719ee 100644
---- a/test/vod-moment-evidence.test.js
++++ b/test/vod-moment-evidence.test.js
@@@ -110,6 +110,11 @@ console.warn = () => {};
assert.strictEqual(await moments.findBestMoment(v, { flavor: 'clip' }), null, 'the AI said nothing is worth it');
aiService.structured = async () => ({ t: null });
assert.strictEqual(await moments.findBestMoment(v, { flavor: 'clip' }), null, 't: null is an abstention too');
++ // A paste is never left empty by a declined pick: the signals choose its frame (11-03).
++ aiService.structured = async () => ({ abstain: true, why: 'nothing stands out' });
++ const pasteBack = await moments.findBestMoment(v, { flavor: 'paste' });
++ assert.ok(pasteBack && Number.isFinite(pasteBack.offset), 'a declined paste falls back to the signals');
++ assert.notStrictEqual(pasteBack.evidence.picked_by, 'ai');
let asked = null;
aiService.structured = async (_name, input) => { asked = input; return { t: 1500, title: 'Lost it', desc: 'The streamer laughs at a clip on the second monitor' }; };
const picked = await moments.findBestMoment(v, { flavor: 'clip' });