openvibers/openvibe.live Commits

AI: resume summarizing pastes via Media

OP OpenVibers committed 19 Aug 2026 · 97252096d8 · 2 files, +38 −1

The paste analysis pass queried this service's local pastes table, which has had
0 rows since pastes moved to Media — so it silently found nothing and no paste
created after the migration ever got a summary.

Poll Media's needs_ai queue instead, analyse with the existing
analyzeTextPaste/analyzeImagePaste, and post the result back. Batches 1 while a
stream is live, 3 when idle, matching the surrounding transcript passes.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Xw6Sf5zzSALw8m93XNu5JE

Changes

ddiff --git a/server/ai/backfill-job.js b/server/ai/backfill-job.js
iindex 609050de..19f6a1ac 100644
---- a/server/ai/backfill-job.js
++++ b/server/ai/backfill-job.js
@@@ -73,6 +73,33 @@ async function tick() {
if (!ai.isEnabled()) return;
++ // Paste summaries. Pastes moved to Media at the migration, but the analysis job
++ // kept querying this service's OWN pastes table — which no longer receives rows —
++ // so every paste created since had no AI at all. Media has no LLM, so it hands us
++ // a work queue and we post results back.
++ if (ai.pasteAnalysisEnabled && ai.pasteAnalysisEnabled()) {
++ try {
++ const out = await media.listPastesNeedingAi(anyLive ? 1 : 3).catch(() => null);
++ for (const p of (out?.pastes || [])) {
++ if (!p || !p.slug) continue;
++ let r = null;
++ if (p.type === 'screenshot') {
++ // Vision needs the actual image; skip when there is no URL to read.
++ const img = p.screenshot_url || null;
++ if (img) r = await ai.analyzeImagePaste(img, p.title).catch(() => null);
++ } else {
++ r = await ai.analyzeTextPaste(p.content || '', p.title).catch(() => null);
++ }
++ if (!r || !r.description) continue;
++ await media.setPasteAi(p.slug, {
++ ai_summary: r.description,
++ ai_tags: JSON.stringify(r.tags || []),
++ }).catch((e) => console.warn('[AI backfill] paste ai write:', e.message));
++ console.log(`[AI backfill] paste ${p.slug} summarized`);
++ }
++ } catch (e) { console.warn('[AI backfill] paste:', e.message); }
++ }
++
// VOD overviews (frames + audio pulled from the Media playback URL; self-marks).
try {
for (const row of db.getVodsNeedingOverview(1)) {
ddiff --git a/server/media-client.js b/server/media-client.js
iindex b82fd468..c72e5a76 100644
---- a/server/media-client.js
++++ b/server/media-client.js
@@@ -219,6 +219,16 @@ function listPastes(query = {}, opts = {}) {
return request('GET', '/pastes', { query, ...opts });
}
++/** Pastes awaiting AI analysis (app-key only on Media). */
++function listPastesNeedingAi(limit = 5, opts = {}) {
++ return request('GET', '/pastes', { query: { needs_ai: 1, limit }, ...opts });
++}
++
++/** Write AI results back to a paste. */
++function setPasteAi(slug, { ai_summary, ai_tags } = {}, opts = {}) {
++ return request('POST', `/pastes/${encodeURIComponent(slug)}/ai`, { body: { ai_summary, ai_tags }, ...opts });
++}
++
function deletePaste(slug, opts = {}) {
return request('DELETE', `/pastes/${encodeURIComponent(slug)}`, opts);
}
@@@ -337,7 +347,7 @@ module.exports = {
// clips
createClip, getClip, listClips, updateClip, deleteClip, recutClip,
// pastes
-- createPaste, getPaste, listPastes, deletePaste,
++ createPaste, getPaste, listPastes, listPastesNeedingAi, setPasteAi, deletePaste,
// files + thumbnails
uploadFile, getFileMeta, deleteFile,
uploadThumbnail, generateThumbnail,