openvibers/openvibe.live Commits
An AI clip is public only when its title and description are backed by the moment (item 151, C09) (#593)
Changes
@@@ -228,6 +228,20 @@ function _evidenceAt(ctx, t, pickedBy) {};}/** Transcript text around a moment, so dedupe can tell a new conversation from a repeated scene. */++/**++ * What is known about the minute around a moment (VOD time), for the verifier (C09) to check a clip's title and++ * description against: scene notes, speech and sounds, each with its second.++ */++function _linesNear(ctx, offset, span = 60) {++ const t = Number(offset) || 0;++ const near = (x) => Number.isFinite(x) && Math.abs(x - t) <= span;++ return {++ timeline: (ctx.memories || []).filter((m) => near(Number(m.offset_seconds)) && m.description).slice(0, 30).map((m) => ({ t: Number(m.offset_seconds), text: _cleanText(_deJson(m.description), 300) })),++ transcript: _rowsNear(ctx.said || ctx.transcript, t, span).map((r) => ({ t: r.start, text: _cleanText(r.text, 300) })),++ sounds: (ctx.heard || ctx.sounds || []).filter((e) => near(Number(e.start_sec)) && e.label).slice(0, 30).map((e) => ({ t: Number(e.start_sec), label: _cleanText(e.label, 80) })),++ };++}++/** The transcript rows within `span` seconds of a moment (VOD time), for the cutter to start and end on sentences. */function _rowsNear(transcript, offset, span = 60) {const t = Number(offset) || 0;@@@ -286,7 +300,7 @@ async function _findBestMoment(vod, { flavor = 'paste', avoid = [] } = {}) {t = clamp(t);if (!farFromUsed(t)) { console.log(`[AI-Moments] VOD ${vod.vod_id}: pick at ${_mmss(t)} is next to a used moment — trying the objective signals instead`); throw new Error('near used'); }const near = _nearestMemory(ctx.memories, t);-- const result = { offset: t, title: _publicTitle(j.title, vod, t, j.desc), desc: _cleanText(_deJson(j.desc), 400) || (near && _deJson(near.description)) || '', speech: _speechNear(ctx.said || ctx.transcript, t), rows: _rowsNear(ctx.said || ctx.transcript, t), tags: _memTags(near), evidence: _evidenceAt(ctx, t, 'ai') };++ const result = { offset: t, title: _publicTitle(j.title, vod, t, j.desc), desc: _cleanText(_deJson(j.desc), 400) || (near && _deJson(near.description)) || '', speech: _speechNear(ctx.said || ctx.transcript, t), rows: _rowsNear(ctx.said || ctx.transcript, t), lines: _linesNear(ctx, t), tags: _memTags(near), evidence: _evidenceAt(ctx, t, 'ai') };if (explicit.isExplicit(j.desc, j.title, near && near.description, result.desc)) { console.log(`[AI-Moments] VOD ${vod.vod_id}: the pick at ${_mmss(t)} shows sexual content — no moment from it`); return null; }return _isEmptyScene(result.desc) ? null : result;}@@@ -309,7 +323,7 @@ async function _findBestMoment(vod, { flavor = 'paste', avoid = [] } = {}) {const near = _nearestMemory(ctx.memories, offset);const desc = near ? _deJson(near.description) : '';if (near && explicit.isExplicit(near.description)) return null; // never a moment of sexual content (server/ai/explicit.js)-- return _isEmptyScene(desc) ? null : { offset, title: _fallbackTitle(vod, offset, desc), desc, speech: _speechNear(ctx.said || ctx.transcript, offset), rows: _rowsNear(ctx.said || ctx.transcript, offset), tags: _memTags(near), evidence: _evidenceAt(ctx, offset, candidates[0].by) };++ return _isEmptyScene(desc) ? null : { offset, title: _fallbackTitle(vod, offset, desc), desc, speech: _speechNear(ctx.said || ctx.transcript, offset), rows: _rowsNear(ctx.said || ctx.transcript, offset), lines: _linesNear(ctx, offset), tags: _memTags(near), evidence: _evidenceAt(ctx, offset, candidates[0].by) };}// True when a description says the frame is essentially empty — black/dark/loading/blank — so we// never turn "nothing" into a clip or paste.@@@ -614,7 +628,7 @@ async function tick(opts = {}) {try {const second = await _findBestMoment(v, { flavor: 'clip', avoid: [offset] });if (second && !await registry.isUsed({ vod_id: v.vod_id, stream_id: v.stream_id, offset: second.offset, desc: second.desc, title: second.title, speech: second.speech })) {-- const clip = await require('./auto-clip-job').clipVodMoment({ vod: v, offset: second.offset, title: second.title, desc: second.desc, speech: second.speech, segments: second.rows, evidence: second.evidence, archival: v.archival, source: momentSource });++ const clip = await require('./auto-clip-job').clipVodMoment({ vod: v, offset: second.offset, title: second.title, desc: second.desc, speech: second.speech, segments: second.rows, lines: second.lines, evidence: second.evidence, archival: v.archival, source: momentSource });if (clip) console.log(`[AI-Moments] Auto-clip cut for VOD ${v.vod_id} at ${_mmss(second.offset)} ("${String(second.title || '').slice(0, 60)}") — paste was at ${_mmss(offset)}`);} else console.log(`[AI-Moments] VOD ${v.vod_id}: no second, distinct beat for a clip — skipped`);} catch { /* clipping is best-effort */ }@@@ -645,7 +659,7 @@ function start() {setInterval(() => { tick().catch(() => {}); }, 5 * 60 * 1000);}--module.exports = { start, tick, findBestMoment: _findBestMoment, frameTooDark: _frameTooDark, makeSlug: _slug, publicTitle: _publicTitle, _internals: { rowsNear: _rowsNear, momentPool: _momentPool, fallbackTitle: _fallbackTitle, captionTitle: _captionTitle, evidenceAt: _evidenceAt, keepHeroFull } };++module.exports = { start, tick, findBestMoment: _findBestMoment, frameTooDark: _frameTooDark, makeSlug: _slug, publicTitle: _publicTitle, _internals: { rowsNear: _rowsNear, linesNear: _linesNear, momentPool: _momentPool, fallbackTitle: _fallbackTitle, captionTitle: _captionTitle, evidenceAt: _evidenceAt, keepHeroFull } };// CLI: force a one-off regeneration, e.g. a whole-dataset "best of all-time" test run:// node server/ai/ai-moments-job.js --fresh --target=6 --perUser=2@@@ -318,7 +318,7 @@ async function backfillVodClips({ limit = BACKFILL_PER_RUN, force = false } = {}if (dark) { console.log(`[AutoClip] backfill skip VOD ${v.vod_id} — dark/empty frame`); newSkip.push(v.vod_id); continue; }}} catch { /* probe optional */ }-- const clip = await clipVodMoment({ vod: v, offset: moment.offset, title: moment.title, desc: moment.desc, speech: moment.speech, segments: moment.rows, evidence: moment.evidence, archival: v.archival, source });++ const clip = await clipVodMoment({ vod: v, offset: moment.offset, title: moment.title, desc: moment.desc, speech: moment.speech, segments: moment.rows, lines: moment.lines, evidence: moment.evidence, archival: v.archival, source });if (clip) { made++; if (v.archival) archiveMade++; console.log(`[AutoClip] Backfilled VOD ${v.vod_id}${v.archival ? ' (archive)' : ''} ("${String(moment.title || '').slice(0, 60)}")`); }else if (v.archival && !clipPolicy.archiveAllowed(await _clipLog(), Date.now())) continue; // another cutter took today's archive slot: not this VOD's fault, don't skip it foreverelse newSkip.push(v.vod_id); // cut failed (e.g. unseekable/short) — don't retry forever@@@ -369,6 +369,26 @@ async function _isDuplicateAutoClip(vod, offset, desc, title, speech) {return false;}++/**++ * The verifier (item 151, C09; OpenVibe.AI live.moments.verify): before an AI clip is public, its title and++ * description are checked against the minute around the moment. → 'supported' | 'unsupported' | 'unsure' | 'failed'.++ * Only "supported" is a yes: a check that fails or cannot tell keeps the clip unlisted.++ */++async function _verifyMoment({ vod, offset, title, desc, lines, evidence }) {++ try {++ const l = lines || {};++ const sp = (evidence && evidence.spike) || {};++ const out = await aiService.structured('live.moments.verify', {++ title: String(vod.title || '').slice(0, 200), clip_title: String(title || '').slice(0, 200), clip_desc: String(desc || '').slice(0, 800),++ t: Math.max(0, Number(offset) || 0),++ timeline: (l.timeline || []).slice(0, 60), transcript: (l.transcript || []).slice(0, 120), sounds: (l.sounds || []).slice(0, 60),++ chat: { peak: Math.max(0, Number(sp.peak) || 0), avg: Math.max(0, Number(sp.avg) || 0), people: Math.max(0, Number(evidence && evidence.chatters) || 0) },++ }, { target: { service: 'live', type: 'vod', id: String(vod.vod_id || vod.id) }, meter: { kind: 'moment_verify', role: 'legacy', ownerUserId: vod.user_id } });++ if (!out || !['supported', 'unsupported', 'unsure'].includes(out.verdict)) return { verdict: 'failed', why: 'no answer' };++ return out;++ } catch (e) { return { verdict: 'failed', why: String(e && e.message || e).slice(0, 120) }; }++}++async function clipVodMoment(o) {let archiveHeld = false;try {@@@ -393,7 +413,18 @@ async function clipVodMoment(o) {// the clip public whatever the recording is.const publication = require('./clip-publication');if (!publication.mayCut(vodId)) { console.log(`[AutoClip] VOD ${vodId}: protected, not cut`); return null; }-- const visibility = publication.visibilityFor({ vodVisibility: await publication.vodVisibility(vod), wanted: rated.verdict === 'public' ? 'public' : 'unlisted' });++ // Public only when the verifier backs the title and description (C09); an unbacked claim is not cut at all.++ let wanted = rated.verdict === 'public' ? 'public' : 'unlisted';++ let verified = null;++ if (wanted === 'public') {++ verified = await _verifyMoment({ vod, offset, title, desc, lines: o.lines, evidence });++ if (verified.verdict === 'unsupported') {++ console.log(`[AutoClip] VOD ${vodId} @${Math.round(offset)}s not cut — the title or description is not backed by the moment${verified.unsupported && verified.unsupported.length ? ` (${verified.unsupported.slice(0, 2).join('; ')})` : ''}`);++ return null;++ }++ if (verified.verdict !== 'supported') wanted = 'unlisted';++ }++ const visibility = publication.visibilityFor({ vodVisibility: await publication.vodVisibility(vod), wanted });// Start and end on sentences (server/ai/clip-trim.js), as a live clip does: the rows near the moment are in the// VOD's own time. Without speech near it the fixed window stands (11-03: VOD clips always used it, so they// began and ended mid-sentence).@@@ -421,9 +452,9 @@ async function clipVodMoment(o) {archiveHeld = false; // the clip exists: keep the day reserved even if the log write failsconst sp = (evidence && evidence.spike) || {};await _logClip({ stream_id: vod.stream_id || null, vod_id: vodId, start_time: start, title: title || 'Standout Moment', clip_id: clip.id, dedup: !!clip.deduplicated || undefined, desc: desc || '', speech, archival: archival || undefined, sig: _sceneSig(desc || title),-- score: rated.score, visibility: rated.verdict, bucket: rated.bucket, peak: Number(sp.peak) || 0, chatters: evidence.chatters == null ? undefined : evidence.chatters,-- picked_by: evidence.picked_by || undefined, policy: VOD_POLICY });-- console.log(`[AutoClip] VOD ${vodId} @${Math.round(offset)}s cut (${evidence.picked_by || 'signals'}) — score ${rated.score} → ${rated.verdict}, bucket ${rated.bucket}${cut.snapped ? ', on sentences' : ''}`);++ score: rated.score, visibility: wanted, bucket: rated.bucket, peak: Number(sp.peak) || 0, chatters: evidence.chatters == null ? undefined : evidence.chatters,++ picked_by: evidence.picked_by || undefined, policy: VOD_POLICY, verified: verified ? verified.verdict : undefined });++ console.log(`[AutoClip] VOD ${vodId} @${Math.round(offset)}s cut (${evidence.picked_by || 'signals'}) — score ${rated.score} → ${wanted}${verified ? ` (check: ${verified.verdict})` : ''}, bucket ${rated.bucket}${cut.snapped ? ', on sentences' : ''}`);}return clip || null;} catch { return null; }@@@ -56,11 +56,19 @@ console.warn = () => {};assert.strictEqual(await autoClip.clipVodMoment({ vod: vod(501), offset: 300, title: 'Just talking', desc: 'A person talks to the camera about lunch', evidence: { picked_by: 'ai', spike: { peak: 0, avg: 0 }, chatters: null, sounds: [], speechWords: 30 } }), null);assert.strictEqual(created.length, 0, 'the thin moment never reached Media');assert.ok(log.some((l) => /VOD 501 @300s not cut — score/.test(l)), 'the abstention is logged with its score');-- const c1 = await autoClip.clipVodMoment({ vod: vod(502), offset: 300, title: 'The room lost it', desc: 'The streamer trips over the cable mid-sentence', evidence: { picked_by: 'ai', spike: { peak: 22, avg: 1.5 }, chatters: 7, sounds: [], speechWords: 25 } });++ // The verifier (C09) answers for public clips only; each call is recorded.++ const verifyCalls = [];++ let verdictFor = () => ({ verdict: 'supported', unsupported: [], why: 'it happens' });++ aiService.structured = async (name, input) => { if (name === 'live.moments.verify') { verifyCalls.push(input); return verdictFor(input); } return null; };++ const lines = { timeline: [{ t: 298, text: 'the streamer trips over a cable' }], transcript: [{ t: 299, text: 'oh no the cable' }], sounds: [] };++ const c1 = await autoClip.clipVodMoment({ vod: vod(502), offset: 300, title: 'The room lost it', desc: 'The streamer trips over the cable mid-sentence', lines, evidence: { picked_by: 'ai', spike: { peak: 22, avg: 1.5 }, chatters: 7, sounds: [], speechWords: 25 } });++ assert.strictEqual(verifyCalls.length, 1, 'a public clip is checked first');++ assert.deepStrictEqual([verifyCalls[0].clip_title, verifyCalls[0].t, verifyCalls[0].timeline[0].text, verifyCalls[0].chat.people], ['The room lost it', 300, 'the streamer trips over a cable', 7]);assert.ok(c1 && c1.id);const c2 = await autoClip.clipVodMoment({ vod: vod(503), offset: 300, title: 'A laugh', desc: 'A dog barks and the streamer laughs at the window', evidence: { picked_by: 'ai', spike: { peak: 0, avg: 0 }, chatters: null, sounds: [{ label: 'Laughter', confidence: 0.8 }], speechWords: 30 } });assert.ok(c2 && c2.id);assert.deepStrictEqual(created.map((b) => b.visibility), ['public', 'unlisted'], 'public only for the reacted room');++ assert.strictEqual(verifyCalls.length, 1, 'an unlisted clip is not checked');assert.deepStrictEqual([created[0].start_s, created[0].end_s], [284, 309], 'no speech near it: the fixed 16 s before and 9 s after');// ── A VOD clip starts and ends on sentences (clip-trim.js), as a live clip does (11-03: it always used the window) ──@@@ -72,6 +80,8 @@ console.warn = () => {};{ start: 306.2, end: 311.0, text: 'Chat, did you see that?' },];const near = moments._internals.rowsNear(rows, 300);++ const ln = moments._internals.linesNear({ memories: [{ offset_seconds: 295, description: 'a desk tips over' }, { offset_seconds: 900, description: 'far' }], said: rows, heard: [{ start_sec: 301, label: 'Laughter' }] }, 300);++ assert.deepStrictEqual([ln.timeline.map((x) => x.text), ln.transcript.length, ln.sounds], [['a desk tips over'], 4, [{ t: 301, label: 'Laughter' }]], 'the verifier reads the minute around the moment');assert.deepStrictEqual(near.map((r) => r.start), [279.2, 286.4, 296.5, 306.2], 'the rows within a minute of the moment');const c3 = await autoClip.clipVodMoment({ vod: { ...vod(504), duration: 3600 }, offset: 300, title: 'Desk goes down', desc: 'The streamer walks into the cable and the desk tips over', segments: near, evidence: { picked_by: 'ai', spike: { peak: 22, avg: 1.5 }, chatters: 7, sounds: [], speechWords: 25 } });assert.ok(c3 && c3.id);@@@ -88,6 +98,29 @@ console.warn = () => {};assert.strictEqual(e1.bucket, 'D');assert.ok(e1.score >= 0.7);++ assert.strictEqual(e1.verified, 'supported');++++ // ── C09: an unbacked title is not cut; "unsure" or a failed check keeps the clip unlisted ──++ const reacted = { picked_by: 'ai', spike: { peak: 22, avg: 1.5 }, chatters: 7, sounds: [], speechWords: 25 };++ verdictFor = () => ({ verdict: 'unsupported', unsupported: ['a dragon explodes'], why: 'no dragon' });++ const before = created.length;++ assert.strictEqual(await autoClip.clipVodMoment({ vod: vod(510), offset: 300, title: 'Dragon Eruption', desc: 'A dragon explodes on screen', lines, evidence: reacted }), null);++ assert.strictEqual(created.length, before, 'not cut');++ assert.ok(log.some((l) => /VOD 510 @300s not cut — the title or description is not backed by the moment \(a dragon explodes\)/.test(l)));++ verdictFor = () => ({ verdict: 'unsure', unsupported: [], why: 'too little' });++ assert.ok(await autoClip.clipVodMoment({ vod: vod(511), offset: 300, title: 'Cable Trip', desc: 'The streamer trips', lines, evidence: reacted }));++ assert.strictEqual(created[created.length - 1].visibility, 'unlisted', 'unsure: kept unlisted');++ verdictFor = () => { throw new Error('AI down'); };++ assert.ok(await autoClip.clipVodMoment({ vod: vod(512), offset: 300, title: 'Cable Trip Again', desc: 'The streamer trips twice', lines, evidence: reacted }));++ assert.strictEqual(created[created.length - 1].visibility, 'unlisted', 'a failed check is not a yes');++ verdictFor = () => ({ verdict: 'yes!' });++ assert.ok(await autoClip.clipVodMoment({ vod: vod(513), offset: 300, title: 'Cable Trip Thrice', desc: 'The streamer trips thrice', lines, evidence: reacted }));++ assert.strictEqual(created[created.length - 1].visibility, 'unlisted', 'an answer outside the set is not a yes');++ const logged = JSON.parse(await db.getState('auto_clip_log'));++ assert.strictEqual(logged.find((e) => e.vod_id === 512).verified, 'failed');++ assert.strictEqual(logged.find((e) => e.vod_id === 512).visibility, 'unlisted', 'the log records what was published');++ verdictFor = () => ({ verdict: 'supported', unsupported: [], why: 'it happens' });++// ── Bench names the policy that cut the clip ──assert.deepStrictEqual(evaluation.policyOf({ id: c1.id }, entries), { selector: 'vod-moment', version: '2026-11-03', models: [] });assert.deepStrictEqual(evaluation.policyOf({ id: 4242 }, entries), { selector: 'auto-clip', version: evaluation.AUTO_CLIP_POLICY, models: [] }, 'an older clip keeps the old label');
@@ -228,6 +228,20 @@ function _evidenceAt(ctx, t, pickedBy) {
@@ -228,6 +228,20 @@ function _evidenceAt(ctx, t, pickedBy) {
};
};
}
}
/** Transcript text around a moment, so dedupe can tell a new conversation from a repeated scene. */
/** Transcript text around a moment, so dedupe can tell a new conversation from a repeated scene. */
+/**
+ * What is known about the minute around a moment (VOD time), for the verifier (C09) to check a clip's title and
+ * description against: scene notes, speech and sounds, each with its second.
+ */
+function _linesNear(ctx, offset, span = 60) {
+ const t = Number(offset) || 0;
+ const near = (x) => Number.isFinite(x) && Math.abs(x - t) <= span;
+ return {
+ timeline: (ctx.memories || []).filter((m) => near(Number(m.offset_seconds)) && m.description).slice(0, 30).map((m) => ({ t: Number(m.offset_seconds), text: _cleanText(_deJson(m.description), 300) })),
+ transcript: _rowsNear(ctx.said || ctx.transcript, t, span).map((r) => ({ t: r.start, text: _cleanText(r.text, 300) })),
+ sounds: (ctx.heard || ctx.sounds || []).filter((e) => near(Number(e.start_sec)) && e.label).slice(0, 30).map((e) => ({ t: Number(e.start_sec), label: _cleanText(e.label, 80) })),
+ };
+}
+
/** The transcript rows within `span` seconds of a moment (VOD time), for the cutter to start and end on sentences. */
/** The transcript rows within `span` seconds of a moment (VOD time), for the cutter to start and end on sentences. */
function _rowsNear(transcript, offset, span = 60) {
function _rowsNear(transcript, offset, span = 60) {
const t = Number(offset) || 0;
const t = Number(offset) || 0;
@@ -286,7 +300,7 @@ async function _findBestMoment(vod, { flavor = 'paste', avoid = [] } = {}) {
@@ -286,7 +300,7 @@ async function _findBestMoment(vod, { flavor = 'paste', avoid = [] } = {}) {
t = clamp(t);
t = clamp(t);
if (!farFromUsed(t)) { console.log(`[AI-Moments] VOD ${vod.vod_id}: pick at ${_mmss(t)} is next to a used moment — trying the objective signals instead`); throw new Error('near used'); }
if (!farFromUsed(t)) { console.log(`[AI-Moments] VOD ${vod.vod_id}: pick at ${_mmss(t)} is next to a used moment — trying the objective signals instead`); throw new Error('near used'); }
const near = _nearestMemory(ctx.memories, t);
const near = _nearestMemory(ctx.memories, t);
- const result = { offset: t, title: _publicTitle(j.title, vod, t, j.desc), desc: _cleanText(_deJson(j.desc), 400) || (near && _deJson(near.description)) || '', speech: _speechNear(ctx.said || ctx.transcript, t), rows: _rowsNear(ctx.said || ctx.transcript, t), tags: _memTags(near), evidence: _evidenceAt(ctx, t, 'ai') };
+ const result = { offset: t, title: _publicTitle(j.title, vod, t, j.desc), desc: _cleanText(_deJson(j.desc), 400) || (near && _deJson(near.description)) || '', speech: _speechNear(ctx.said || ctx.transcript, t), rows: _rowsNear(ctx.said || ctx.transcript, t), lines: _linesNear(ctx, t), tags: _memTags(near), evidence: _evidenceAt(ctx, t, 'ai') };
if (explicit.isExplicit(j.desc, j.title, near && near.description, result.desc)) { console.log(`[AI-Moments] VOD ${vod.vod_id}: the pick at ${_mmss(t)} shows sexual content — no moment from it`); return null; }
if (explicit.isExplicit(j.desc, j.title, near && near.description, result.desc)) { console.log(`[AI-Moments] VOD ${vod.vod_id}: the pick at ${_mmss(t)} shows sexual content — no moment from it`); return null; }
return _isEmptyScene(result.desc) ? null : result;
return _isEmptyScene(result.desc) ? null : result;
}
}
@@ -309,7 +323,7 @@ async function _findBestMoment(vod, { flavor = 'paste', avoid = [] } = {}) {
@@ -309,7 +323,7 @@ async function _findBestMoment(vod, { flavor = 'paste', avoid = [] } = {}) {
const near = _nearestMemory(ctx.memories, offset);
const near = _nearestMemory(ctx.memories, offset);
const desc = near ? _deJson(near.description) : '';
const desc = near ? _deJson(near.description) : '';
if (near && explicit.isExplicit(near.description)) return null; // never a moment of sexual content (server/ai/explicit.js)
if (near && explicit.isExplicit(near.description)) return null; // never a moment of sexual content (server/ai/explicit.js)
- return _isEmptyScene(desc) ? null : { offset, title: _fallbackTitle(vod, offset, desc), desc, speech: _speechNear(ctx.said || ctx.transcript, offset), rows: _rowsNear(ctx.said || ctx.transcript, offset), tags: _memTags(near), evidence: _evidenceAt(ctx, offset, candidates[0].by) };
+ return _isEmptyScene(desc) ? null : { offset, title: _fallbackTitle(vod, offset, desc), desc, speech: _speechNear(ctx.said || ctx.transcript, offset), rows: _rowsNear(ctx.said || ctx.transcript, offset), lines: _linesNear(ctx, offset), tags: _memTags(near), evidence: _evidenceAt(ctx, offset, candidates[0].by) };
}
}
// True when a description says the frame is essentially empty — black/dark/loading/blank — so we
// True when a description says the frame is essentially empty — black/dark/loading/blank — so we
// never turn "nothing" into a clip or paste.
// never turn "nothing" into a clip or paste.
@@ -614,7 +628,7 @@ async function tick(opts = {}) {
@@ -614,7 +628,7 @@ async function tick(opts = {}) {
try {
try {
const second = await _findBestMoment(v, { flavor: 'clip', avoid: [offset] });
const second = await _findBestMoment(v, { flavor: 'clip', avoid: [offset] });
if (second && !await registry.isUsed({ vod_id: v.vod_id, stream_id: v.stream_id, offset: second.offset, desc: second.desc, title: second.title, speech: second.speech })) {
if (second && !await registry.isUsed({ vod_id: v.vod_id, stream_id: v.stream_id, offset: second.offset, desc: second.desc, title: second.title, speech: second.speech })) {
- const clip = await require('./auto-clip-job').clipVodMoment({ vod: v, offset: second.offset, title: second.title, desc: second.desc, speech: second.speech, segments: second.rows, evidence: second.evidence, archival: v.archival, source: momentSource });
+ const clip = await require('./auto-clip-job').clipVodMoment({ vod: v, offset: second.offset, title: second.title, desc: second.desc, speech: second.speech, segments: second.rows, lines: second.lines, evidence: second.evidence, archival: v.archival, source: momentSource });
if (clip) console.log(`[AI-Moments] Auto-clip cut for VOD ${v.vod_id} at ${_mmss(second.offset)} ("${String(second.title || '').slice(0, 60)}") — paste was at ${_mmss(offset)}`);
if (clip) console.log(`[AI-Moments] Auto-clip cut for VOD ${v.vod_id} at ${_mmss(second.offset)} ("${String(second.title || '').slice(0, 60)}") — paste was at ${_mmss(offset)}`);
} else console.log(`[AI-Moments] VOD ${v.vod_id}: no second, distinct beat for a clip — skipped`);
} else console.log(`[AI-Moments] VOD ${v.vod_id}: no second, distinct beat for a clip — skipped`);
} catch { /* clipping is best-effort */ }
} catch { /* clipping is best-effort */ }
@@ -645,7 +659,7 @@ function start() {
@@ -645,7 +659,7 @@ function start() {
setInterval(() => { tick().catch(() => {}); }, 5 * 60 * 1000);
setInterval(() => { tick().catch(() => {}); }, 5 * 60 * 1000);
}
}
-module.exports = { start, tick, findBestMoment: _findBestMoment, frameTooDark: _frameTooDark, makeSlug: _slug, publicTitle: _publicTitle, _internals: { rowsNear: _rowsNear, momentPool: _momentPool, fallbackTitle: _fallbackTitle, captionTitle: _captionTitle, evidenceAt: _evidenceAt, keepHeroFull } };
+module.exports = { start, tick, findBestMoment: _findBestMoment, frameTooDark: _frameTooDark, makeSlug: _slug, publicTitle: _publicTitle, _internals: { rowsNear: _rowsNear, linesNear: _linesNear, momentPool: _momentPool, fallbackTitle: _fallbackTitle, captionTitle: _captionTitle, evidenceAt: _evidenceAt, keepHeroFull } };
// CLI: force a one-off regeneration, e.g. a whole-dataset "best of all-time" test run:
// CLI: force a one-off regeneration, e.g. a whole-dataset "best of all-time" test run:
// node server/ai/ai-moments-job.js --fresh --target=6 --perUser=2
// node server/ai/ai-moments-job.js --fresh --target=6 --perUser=2
@@ -318,7 +318,7 @@ async function backfillVodClips({ limit = BACKFILL_PER_RUN, force = false } = {}
@@ -318,7 +318,7 @@ async function backfillVodClips({ limit = BACKFILL_PER_RUN, force = false } = {}
if (dark) { console.log(`[AutoClip] backfill skip VOD ${v.vod_id} — dark/empty frame`); newSkip.push(v.vod_id); continue; }
if (dark) { console.log(`[AutoClip] backfill skip VOD ${v.vod_id} — dark/empty frame`); newSkip.push(v.vod_id); continue; }
}
}
} catch { /* probe optional */ }
} catch { /* probe optional */ }
- const clip = await clipVodMoment({ vod: v, offset: moment.offset, title: moment.title, desc: moment.desc, speech: moment.speech, segments: moment.rows, evidence: moment.evidence, archival: v.archival, source });
+ const clip = await clipVodMoment({ vod: v, offset: moment.offset, title: moment.title, desc: moment.desc, speech: moment.speech, segments: moment.rows, lines: moment.lines, evidence: moment.evidence, archival: v.archival, source });
if (clip) { made++; if (v.archival) archiveMade++; console.log(`[AutoClip] Backfilled VOD ${v.vod_id}${v.archival ? ' (archive)' : ''} ("${String(moment.title || '').slice(0, 60)}")`); }
if (clip) { made++; if (v.archival) archiveMade++; console.log(`[AutoClip] Backfilled VOD ${v.vod_id}${v.archival ? ' (archive)' : ''} ("${String(moment.title || '').slice(0, 60)}")`); }
else if (v.archival && !clipPolicy.archiveAllowed(await _clipLog(), Date.now())) continue; // another cutter took today's archive slot: not this VOD's fault, don't skip it forever
else if (v.archival && !clipPolicy.archiveAllowed(await _clipLog(), Date.now())) continue; // another cutter took today's archive slot: not this VOD's fault, don't skip it forever
else newSkip.push(v.vod_id); // cut failed (e.g. unseekable/short) — don't retry forever
else newSkip.push(v.vod_id); // cut failed (e.g. unseekable/short) — don't retry forever
@@ -369,6 +369,26 @@ async function _isDuplicateAutoClip(vod, offset, desc, title, speech) {
@@ -369,6 +369,26 @@ async function _isDuplicateAutoClip(vod, offset, desc, title, speech) {
return false;
return false;
}
}
+/**
+ * The verifier (item 151, C09; OpenVibe.AI live.moments.verify): before an AI clip is public, its title and
+ * description are checked against the minute around the moment. → 'supported' | 'unsupported' | 'unsure' | 'failed'.
+ * Only "supported" is a yes: a check that fails or cannot tell keeps the clip unlisted.
+ */
+async function _verifyMoment({ vod, offset, title, desc, lines, evidence }) {
+ try {
+ const l = lines || {};
+ const sp = (evidence && evidence.spike) || {};
+ const out = await aiService.structured('live.moments.verify', {
+ title: String(vod.title || '').slice(0, 200), clip_title: String(title || '').slice(0, 200), clip_desc: String(desc || '').slice(0, 800),
+ t: Math.max(0, Number(offset) || 0),
+ timeline: (l.timeline || []).slice(0, 60), transcript: (l.transcript || []).slice(0, 120), sounds: (l.sounds || []).slice(0, 60),
+ chat: { peak: Math.max(0, Number(sp.peak) || 0), avg: Math.max(0, Number(sp.avg) || 0), people: Math.max(0, Number(evidence && evidence.chatters) || 0) },
+ }, { target: { service: 'live', type: 'vod', id: String(vod.vod_id || vod.id) }, meter: { kind: 'moment_verify', role: 'legacy', ownerUserId: vod.user_id } });
+ if (!out || !['supported', 'unsupported', 'unsure'].includes(out.verdict)) return { verdict: 'failed', why: 'no answer' };
+ return out;
+ } catch (e) { return { verdict: 'failed', why: String(e && e.message || e).slice(0, 120) }; }
+}
+
async function clipVodMoment(o) {
async function clipVodMoment(o) {
let archiveHeld = false;
let archiveHeld = false;
try {
try {
@@ -393,7 +413,18 @@ async function clipVodMoment(o) {
@@ -393,7 +413,18 @@ async function clipVodMoment(o) {
// the clip public whatever the recording is.
// the clip public whatever the recording is.
const publication = require('./clip-publication');
const publication = require('./clip-publication');
if (!publication.mayCut(vodId)) { console.log(`[AutoClip] VOD ${vodId}: protected, not cut`); return null; }
if (!publication.mayCut(vodId)) { console.log(`[AutoClip] VOD ${vodId}: protected, not cut`); return null; }
- const visibility = publication.visibilityFor({ vodVisibility: await publication.vodVisibility(vod), wanted: rated.verdict === 'public' ? 'public' : 'unlisted' });
+ // Public only when the verifier backs the title and description (C09); an unbacked claim is not cut at all.
+ let wanted = rated.verdict === 'public' ? 'public' : 'unlisted';
+ let verified = null;
+ if (wanted === 'public') {
+ verified = await _verifyMoment({ vod, offset, title, desc, lines: o.lines, evidence });
+ if (verified.verdict === 'unsupported') {
+ console.log(`[AutoClip] VOD ${vodId} @${Math.round(offset)}s not cut — the title or description is not backed by the moment${verified.unsupported && verified.unsupported.length ? ` (${verified.unsupported.slice(0, 2).join('; ')})` : ''}`);
+ return null;
+ }
+ if (verified.verdict !== 'supported') wanted = 'unlisted';
+ }
+ const visibility = publication.visibilityFor({ vodVisibility: await publication.vodVisibility(vod), wanted });
// Start and end on sentences (server/ai/clip-trim.js), as a live clip does: the rows near the moment are in the
// Start and end on sentences (server/ai/clip-trim.js), as a live clip does: the rows near the moment are in the
// VOD's own time. Without speech near it the fixed window stands (11-03: VOD clips always used it, so they
// VOD's own time. Without speech near it the fixed window stands (11-03: VOD clips always used it, so they
// began and ended mid-sentence).
// began and ended mid-sentence).
@@ -421,9 +452,9 @@ async function clipVodMoment(o) {
@@ -421,9 +452,9 @@ async function clipVodMoment(o) {
archiveHeld = false; // the clip exists: keep the day reserved even if the log write fails
archiveHeld = false; // the clip exists: keep the day reserved even if the log write fails
const sp = (evidence && evidence.spike) || {};
const sp = (evidence && evidence.spike) || {};
await _logClip({ stream_id: vod.stream_id || null, vod_id: vodId, start_time: start, title: title || 'Standout Moment', clip_id: clip.id, dedup: !!clip.deduplicated || undefined, desc: desc || '', speech, archival: archival || undefined, sig: _sceneSig(desc || title),
await _logClip({ stream_id: vod.stream_id || null, vod_id: vodId, start_time: start, title: title || 'Standout Moment', clip_id: clip.id, dedup: !!clip.deduplicated || undefined, desc: desc || '', speech, archival: archival || undefined, sig: _sceneSig(desc || title),
- score: rated.score, visibility: rated.verdict, bucket: rated.bucket, peak: Number(sp.peak) || 0, chatters: evidence.chatters == null ? undefined : evidence.chatters,
- picked_by: evidence.picked_by || undefined, policy: VOD_POLICY });
- console.log(`[AutoClip] VOD ${vodId} @${Math.round(offset)}s cut (${evidence.picked_by || 'signals'}) — score ${rated.score} → ${rated.verdict}, bucket ${rated.bucket}${cut.snapped ? ', on sentences' : ''}`);
+ score: rated.score, visibility: wanted, bucket: rated.bucket, peak: Number(sp.peak) || 0, chatters: evidence.chatters == null ? undefined : evidence.chatters,
+ picked_by: evidence.picked_by || undefined, policy: VOD_POLICY, verified: verified ? verified.verdict : undefined });
+ console.log(`[AutoClip] VOD ${vodId} @${Math.round(offset)}s cut (${evidence.picked_by || 'signals'}) — score ${rated.score} → ${wanted}${verified ? ` (check: ${verified.verdict})` : ''}, bucket ${rated.bucket}${cut.snapped ? ', on sentences' : ''}`);
}
}
return clip || null;
return clip || null;
} catch { return null; }
} catch { return null; }
@@ -56,11 +56,19 @@ console.warn = () => {};
@@ -56,11 +56,19 @@ console.warn = () => {};
assert.strictEqual(await autoClip.clipVodMoment({ vod: vod(501), offset: 300, title: 'Just talking', desc: 'A person talks to the camera about lunch', evidence: { picked_by: 'ai', spike: { peak: 0, avg: 0 }, chatters: null, sounds: [], speechWords: 30 } }), null);
assert.strictEqual(await autoClip.clipVodMoment({ vod: vod(501), offset: 300, title: 'Just talking', desc: 'A person talks to the camera about lunch', evidence: { picked_by: 'ai', spike: { peak: 0, avg: 0 }, chatters: null, sounds: [], speechWords: 30 } }), null);
assert.strictEqual(created.length, 0, 'the thin moment never reached Media');
assert.strictEqual(created.length, 0, 'the thin moment never reached Media');
assert.ok(log.some((l) => /VOD 501 @300s not cut — score/.test(l)), 'the abstention is logged with its score');
assert.ok(log.some((l) => /VOD 501 @300s not cut — score/.test(l)), 'the abstention is logged with its score');
- const c1 = await autoClip.clipVodMoment({ vod: vod(502), offset: 300, title: 'The room lost it', desc: 'The streamer trips over the cable mid-sentence', evidence: { picked_by: 'ai', spike: { peak: 22, avg: 1.5 }, chatters: 7, sounds: [], speechWords: 25 } });
+ // The verifier (C09) answers for public clips only; each call is recorded.
+ const verifyCalls = [];
+ let verdictFor = () => ({ verdict: 'supported', unsupported: [], why: 'it happens' });
+ aiService.structured = async (name, input) => { if (name === 'live.moments.verify') { verifyCalls.push(input); return verdictFor(input); } return null; };
+ const lines = { timeline: [{ t: 298, text: 'the streamer trips over a cable' }], transcript: [{ t: 299, text: 'oh no the cable' }], sounds: [] };
+ const c1 = await autoClip.clipVodMoment({ vod: vod(502), offset: 300, title: 'The room lost it', desc: 'The streamer trips over the cable mid-sentence', lines, evidence: { picked_by: 'ai', spike: { peak: 22, avg: 1.5 }, chatters: 7, sounds: [], speechWords: 25 } });
+ assert.strictEqual(verifyCalls.length, 1, 'a public clip is checked first');
+ assert.deepStrictEqual([verifyCalls[0].clip_title, verifyCalls[0].t, verifyCalls[0].timeline[0].text, verifyCalls[0].chat.people], ['The room lost it', 300, 'the streamer trips over a cable', 7]);
assert.ok(c1 && c1.id);
assert.ok(c1 && c1.id);
const c2 = await autoClip.clipVodMoment({ vod: vod(503), offset: 300, title: 'A laugh', desc: 'A dog barks and the streamer laughs at the window', evidence: { picked_by: 'ai', spike: { peak: 0, avg: 0 }, chatters: null, sounds: [{ label: 'Laughter', confidence: 0.8 }], speechWords: 30 } });
const c2 = await autoClip.clipVodMoment({ vod: vod(503), offset: 300, title: 'A laugh', desc: 'A dog barks and the streamer laughs at the window', evidence: { picked_by: 'ai', spike: { peak: 0, avg: 0 }, chatters: null, sounds: [{ label: 'Laughter', confidence: 0.8 }], speechWords: 30 } });
assert.ok(c2 && c2.id);
assert.ok(c2 && c2.id);
assert.deepStrictEqual(created.map((b) => b.visibility), ['public', 'unlisted'], 'public only for the reacted room');
assert.deepStrictEqual(created.map((b) => b.visibility), ['public', 'unlisted'], 'public only for the reacted room');
+ assert.strictEqual(verifyCalls.length, 1, 'an unlisted clip is not checked');
assert.deepStrictEqual([created[0].start_s, created[0].end_s], [284, 309], 'no speech near it: the fixed 16 s before and 9 s after');
assert.deepStrictEqual([created[0].start_s, created[0].end_s], [284, 309], 'no speech near it: the fixed 16 s before and 9 s after');
// ── A VOD clip starts and ends on sentences (clip-trim.js), as a live clip does (11-03: it always used the window) ──
// ── A VOD clip starts and ends on sentences (clip-trim.js), as a live clip does (11-03: it always used the window) ──
@@ -72,6 +80,8 @@ console.warn = () => {};
@@ -72,6 +80,8 @@ console.warn = () => {};
{ start: 306.2, end: 311.0, text: 'Chat, did you see that?' },
{ start: 306.2, end: 311.0, text: 'Chat, did you see that?' },
];
];
const near = moments._internals.rowsNear(rows, 300);
const near = moments._internals.rowsNear(rows, 300);
+ const ln = moments._internals.linesNear({ memories: [{ offset_seconds: 295, description: 'a desk tips over' }, { offset_seconds: 900, description: 'far' }], said: rows, heard: [{ start_sec: 301, label: 'Laughter' }] }, 300);
+ assert.deepStrictEqual([ln.timeline.map((x) => x.text), ln.transcript.length, ln.sounds], [['a desk tips over'], 4, [{ t: 301, label: 'Laughter' }]], 'the verifier reads the minute around the moment');
assert.deepStrictEqual(near.map((r) => r.start), [279.2, 286.4, 296.5, 306.2], 'the rows within a minute of the moment');
assert.deepStrictEqual(near.map((r) => r.start), [279.2, 286.4, 296.5, 306.2], 'the rows within a minute of the moment');
const c3 = await autoClip.clipVodMoment({ vod: { ...vod(504), duration: 3600 }, offset: 300, title: 'Desk goes down', desc: 'The streamer walks into the cable and the desk tips over', segments: near, evidence: { picked_by: 'ai', spike: { peak: 22, avg: 1.5 }, chatters: 7, sounds: [], speechWords: 25 } });
const c3 = await autoClip.clipVodMoment({ vod: { ...vod(504), duration: 3600 }, offset: 300, title: 'Desk goes down', desc: 'The streamer walks into the cable and the desk tips over', segments: near, evidence: { picked_by: 'ai', spike: { peak: 22, avg: 1.5 }, chatters: 7, sounds: [], speechWords: 25 } });
assert.ok(c3 && c3.id);
assert.ok(c3 && c3.id);
@@ -88,6 +98,29 @@ console.warn = () => {};
@@ -88,6 +98,29 @@ console.warn = () => {};
assert.strictEqual(e1.bucket, 'D');
assert.strictEqual(e1.bucket, 'D');
assert.ok(e1.score >= 0.7);
assert.ok(e1.score >= 0.7);
+ assert.strictEqual(e1.verified, 'supported');
+
+ // ── C09: an unbacked title is not cut; "unsure" or a failed check keeps the clip unlisted ──
+ const reacted = { picked_by: 'ai', spike: { peak: 22, avg: 1.5 }, chatters: 7, sounds: [], speechWords: 25 };
+ verdictFor = () => ({ verdict: 'unsupported', unsupported: ['a dragon explodes'], why: 'no dragon' });
+ const before = created.length;
+ assert.strictEqual(await autoClip.clipVodMoment({ vod: vod(510), offset: 300, title: 'Dragon Eruption', desc: 'A dragon explodes on screen', lines, evidence: reacted }), null);
+ assert.strictEqual(created.length, before, 'not cut');
+ assert.ok(log.some((l) => /VOD 510 @300s not cut — the title or description is not backed by the moment \(a dragon explodes\)/.test(l)));
+ verdictFor = () => ({ verdict: 'unsure', unsupported: [], why: 'too little' });
+ assert.ok(await autoClip.clipVodMoment({ vod: vod(511), offset: 300, title: 'Cable Trip', desc: 'The streamer trips', lines, evidence: reacted }));
+ assert.strictEqual(created[created.length - 1].visibility, 'unlisted', 'unsure: kept unlisted');
+ verdictFor = () => { throw new Error('AI down'); };
+ assert.ok(await autoClip.clipVodMoment({ vod: vod(512), offset: 300, title: 'Cable Trip Again', desc: 'The streamer trips twice', lines, evidence: reacted }));
+ assert.strictEqual(created[created.length - 1].visibility, 'unlisted', 'a failed check is not a yes');
+ verdictFor = () => ({ verdict: 'yes!' });
+ assert.ok(await autoClip.clipVodMoment({ vod: vod(513), offset: 300, title: 'Cable Trip Thrice', desc: 'The streamer trips thrice', lines, evidence: reacted }));
+ assert.strictEqual(created[created.length - 1].visibility, 'unlisted', 'an answer outside the set is not a yes');
+ const logged = JSON.parse(await db.getState('auto_clip_log'));
+ assert.strictEqual(logged.find((e) => e.vod_id === 512).verified, 'failed');
+ assert.strictEqual(logged.find((e) => e.vod_id === 512).visibility, 'unlisted', 'the log records what was published');
+ verdictFor = () => ({ verdict: 'supported', unsupported: [], why: 'it happens' });
+
// ── Bench names the policy that cut the clip ──
// ── Bench names the policy that cut the clip ──
assert.deepStrictEqual(evaluation.policyOf({ id: c1.id }, entries), { selector: 'vod-moment', version: '2026-11-03', models: [] });
assert.deepStrictEqual(evaluation.policyOf({ id: c1.id }, entries), { selector: 'vod-moment', version: '2026-11-03', models: [] });
assert.deepStrictEqual(evaluation.policyOf({ id: 4242 }, entries), { selector: 'auto-clip', version: evaluation.AUTO_CLIP_POLICY, models: [] }, 'an older clip keeps the old label');
assert.deepStrictEqual(evaluation.policyOf({ id: 4242 }, entries), { selector: 'auto-clip', version: evaluation.AUTO_CLIP_POLICY, models: [] }, 'an older clip keeps the old label');