openvibers/openvibe.ai Commits

live.moments.pick is a versioned template, not a prompt Live renders (roadmap WS-O task 2). The prompt from Live's ai-moments-job.js is the template live.moments.pick, with JSON output t, title and desc. The prepare hook formats the timeline, transcript and sounds as [m:ss] lines, names the clip and chat signals, adds the paste or clip hint, and writes the already-used line only when there is something to avoid; nothing to work from is no run. The postprocess reads the model's JSON as Live did. The workflow takes structured input (title, flavor, timeline, transcript, sounds, clipped, spikes, avoid) and leaves the passthrough list. test/moments-template.test.js checks the rendering against Live's and the parsing; workflows.test runs it end to end on the stub. npm test passes.

OP OpenVibers committed 28 Sept 2026 · 2574aade6b · 4 files, +104 −1


Changes

ddiff --git a/server/workflows/hooks.js b/server/workflows/hooks.js
iindex c564658..d66a68f 100644
---- a/server/workflows/hooks.js
++++ b/server/workflows/hooks.js
@@@ -68,7 +68,32 @@ function sourcesBlock(sources) {
}).join('\n\n');
}
++// Moments (ai-moments-job.js): how each flavor of moment is picked.
++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.',
++};
++const oneLine = (t, max) => String(t || '').replace(/\s+/g, ' ').trim().slice(0, max);
++
const PREPARE = {
++ 'live.moments.pick'(input) {
++ const lines = (list, max) => (list || []).map((x) => `[${mmss(Number(x.t) || 0)}] ${oneLine(x.text || x.label, max)}`).join('\n');
++ 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 };
++ 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),
++ flavor_hint: MOMENT_FLAVOR[input.flavor] || MOMENT_FLAVOR.paste,
++ avoid: avoid.map(mmss).join(', '),
++ },
++ };
++ },
'live.translate'(input) {
const text = String(input.text || '').trim();
const from = input.from || 'auto';
@@@ -163,6 +188,12 @@ const PREPARE = {
};
const POSTPROCESS = {
++ 'live.moments.pick'(r) {
++ const j = r.json || parseJsonLoose(r.text);
++ const t = j && Number(j.t);
++ if (!j || !Number.isFinite(t) || t < 0) return null;
++ return { t: Math.floor(t), title: oneLine(j.title, 80).replace(/^["']+|["']+$/g, ''), desc: oneLine(j.desc, 400) };
++ },
'ai.classify'(r, input) {
const j = r.json || parseJsonLoose(r.text);
if (!j || !input.labels.includes(j.label)) return null; // a label outside the given set is not an answer
ddiff --git a/server/workflows/live.js b/server/workflows/live.js
iindex 16011d8..8adf7d5 100644
---- a/server/workflows/live.js
++++ b/server/workflows/live.js
@@@ -79,6 +79,21 @@ const templates = [
user_prompt: 'You are writing an AI overview of a recorded video. Using ONLY the signals below (be concrete, don\'t invent), summarize what the video is about in 2-5 sentences — the main activities, topics, and vibe.\n\n{{parts}}',
default_route: 'live.legacy', owner: 'live',
},
++ {
++ key: 'live.moments.pick', name: 'Live: pick one moment of a VOD',
++ description: 'From ai-moments-job.js _findBestMoment (moment_pick): the most interesting visible moment for a screenshot paste or a clip, away from moments already used.',
++ input_schema: { type: 'object' },
++ output_schema: {
++ type: 'object', additionalProperties: false, required: ['t', 'title', 'desc'], properties: {
++ t: { type: 'integer', description: 'seconds into the VOD' },
++ title: { type: 'string', description: 'specific punchy 3-8 word title, not the stream name, funny, never generic' },
++ desc: { type: 'string', description: 'one vivid sentence describing the moment' },
++ },
++ },
++ system_prompt: '',
++ user_prompt: 'Below is a single livestream VOD titled "{{title}}", described by its on-screen TIMELINE (visual scene notes) and its AUDIO TRANSCRIPT, each line timestamped [m:ss].\n\nTIMELINE:\n{{timeline}}\n\nTRANSCRIPT:\n{{transcript}}\n\nSOUNDS HEARD (non-speech audio events):\n{{sounds}}\n\nViewers CLIPPED these timestamps (very strong "this was a highlight" signal): {{clipped}}\nChat activity SPIKED around: {{spikes}}\n\n{{flavor_hint}}\n{{#avoid}}ALREADY USED (a paste or clip exists there) — do NOT pick these or anything within 2 minutes of them: {{avoid}}. Find a DIFFERENT moment.{{/avoid}}\n\nFind the SINGLE most interesting/funny/dramatic/surprising/striking moment in this VOD with something clearly VISIBLE happening. Prefer moments backed by the clip/chat signals when they line up with something notable. NEVER pick a black/dark/loading/blank screen, an intro/BRB card, or a moment with no visible content or activity. Return STRICT JSON only, nothing else: {"t": <seconds into the vod>, "title": "<specific punchy 3-8 word title, not the stream name, funny, never generic>", "desc": "<one vivid sentence describing the moment>"}',
++ default_route: 'live.legacy', owner: 'live', metadata: { json_name: 'moment_pick' },
++ },
{
key: 'live.stream.recap', name: 'Live: after-show report',
description: 'From recap/recap.js aiWriteup (stream_recap).',
@@@ -176,6 +191,22 @@ const structured = [
steps: [{ kind: 'llm', template: 'live.media.overview', operation: 'summarize', output: 'text', prepare: 'live.media.overview', postprocess: 'live.media.overview', params: { max_tokens: 400 } }],
cache_mode: 'private',
},
++ {
++ key: 'live.moments.pick', name: 'Pick one moment of a VOD (a screenshot paste or a clip)', namespace: 'live',
++ description: 'The prompt Live used to render (ai-moments-job.js), now a versioned template: timeline, transcript, sounds, clip and chat signals in; one moment out.',
++ input_schema: TEXT_OUT({
++ title: STR(200), flavor: { enum: ['paste', 'clip'] },
++ timeline: { type: 'array', maxItems: 400, items: { type: 'object', required: ['t', 'text'], additionalProperties: false, properties: { t: { type: 'number', minimum: 0 }, text: STR(2000) } } },
++ transcript: { type: 'array', maxItems: 1500, items: { type: 'object', required: ['t', 'text'], additionalProperties: false, properties: { t: { type: 'number', minimum: 0 }, text: STR(2000) } } },
++ sounds: { type: 'array', maxItems: 500, items: { type: 'object', required: ['t', 'label'], additionalProperties: false, properties: { t: { type: 'number', minimum: 0 }, label: STR(120) } } },
++ clipped: { type: 'array', maxItems: 100, items: { type: 'number', minimum: 0 } },
++ spikes: { type: 'array', maxItems: 100, items: { type: 'number', minimum: 0 } },
++ avoid: { type: 'array', maxItems: 500, items: { type: 'number', minimum: 0 } },
++ }, ['title']),
++ output_schema: TEXT_OUT({ t: { type: 'integer', minimum: 0 }, title: STR(90), desc: STR(420) }, ['t', 'title', 'desc']),
++ steps: [{ kind: 'llm', template: 'live.moments.pick', operation: 'generate', output: 'json', prepare: 'live.moments.pick', postprocess: 'live.moments.pick', params: { max_tokens: 300 } }],
++ cache_mode: 'none',
++ },
{
key: 'live.stream.recap', name: 'After-show report for a finished stream', namespace: 'live',
input_schema: TEXT_OUT({ facts: { type: 'object' }, sources: SOURCES(0, 50) }, ['facts']),
@@@ -233,7 +264,6 @@ const PASSTHROUGH = [
['live.viewers.fold', 'AI viewers: memory fold', 'viewers/fold.js — ai_viewers_fold.'],
['live.chat.insight', 'Chat insights and session overviews', 'chat-ai.js and chat-ai-routes.js — chat_global, chat_user, chat_relay, chat_anon, combined_overview, session_titles.'],
['live.moments.rank', 'Moments: rank VOD moments', 'ai-moments-job.js — moment_vod_rank.'],
-- ['live.moments.pick', 'Moments: pick a moment', 'ai-moments-job.js — moment_pick.'],
['live.clips.confirm', 'Auto-clip confirmation', 'auto-clip-job.js — auto_clip_confirm.'],
['live.hero.slogans', 'Home hero slogans', 'slogan-job.js — hero_slogans.'],
['live.easter_egg', 'Easter eggs', 'easter-egg-job.js — easter_egg.'],
ddiff --git a/test/moments-template.test.js b/test/moments-template.test.js
nnew file mode 100644
iindex 0000000..17fb8aa
---- /dev/null
++++ b/test/moments-template.test.js
@@@ -0,0 +1,40 @@
++'use strict';
++// live.moments.pick moved from a prompt Live rendered to a versioned template (roadmap WS-O task 2): the prepare
++// hook formats the timeline, transcript and sounds as [m:ss] lines like Live did, 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 postprocess reads the model's JSON and refuses anything without a time.
++const assert = require('assert');
++const { suite } = require('./helpers');
++const { PREPARE, POSTPROCESS } = require('../server/workflows/hooks');
++const { render } = require('../server/templates');
++const live = require('../server/workflows/live');
++
++const t = suite('moments-template');
++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 out = render(tpl.user_prompt, prep.vars);
++ assert.match(out, /titled "Rust night"/);
++ assert.match(out, /TIMELINE:\n\[1:35\] streamer jumps up/);
++ assert.match(out, /TRANSCRIPT:\n\[1:34\] IT COMPILES/);
++ 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\./);
++ 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');
++ assert.match(none, /TRANSCRIPT:\n\(none\)/);
++ assert.deepStrictEqual(PREPARE['live.moments.pick']({ title: 'x' }), { output: null }, 'nothing to work from: no run');
++});
++
++t.test('the answer is read like Live read it', () => {
++ const post = POSTPROCESS['live.moments.pick'];
++ assert.deepStrictEqual(post({ text: 'Sure! {"t": 95.7, "title": "\\"It Compiles\\"", "desc": "He jumps."}' }), { t: 95, title: 'It Compiles', desc: 'He jumps.' });
++ assert.strictEqual(post({ text: '{"title": "no time"}' }), null);
++ assert.strictEqual(post({ text: 'no json at all' }), null);
++});
++
++t.run();
ddiff --git a/test/workflows.test.js b/test/workflows.test.js
iindex 366ad5a..fe397f6 100644
---- a/test/workflows.test.js
++++ b/test/workflows.test.js
@@@ -47,6 +47,8 @@ const EXAMPLES = {
'tools.describe': { tool: 'img.convert', sources: [{ source_type: 'tools.tool', title: 'img.convert descriptor', content: '{"id":"img.convert","inputs":["file","format"]}' }] },
};
for (const k of new Set(Object.values(KIND_TO_WORKFLOW).concat('live.complete'))) EXAMPLES[k] = PASS;
++// Moved from passthrough to a versioned template (WS-O task 2): structured input.
++EXAMPLES['live.moments.pick'] = { title: 'Rust night', flavor: 'clip', timeline: [{ t: 12, text: 'the build fails' }, { t: 95, text: 'streamer jumps up cheering' }], transcript: [{ t: 94, text: 'IT COMPILES' }], sounds: [{ t: 96, label: 'Cheering' }], clipped: [95], spikes: [96], avoid: [300] };
const t = suite('workflows');
const HAS_FFMPEG = require('child_process').spawnSync('ffprobe', ['-version']).status === 0;