openvibers/openvibe.mediahub Commits

Public AI text never describes a person: Video's AI overview and the recording and clip overviews pass openvibe-sdk/no-looks (v0.61.0) when stored and read (#108)

OP OpenVibers committed 22 days ago · 3876ebbf9c · 7 files, +25 −10

Changes

ddiff --git a/README.md b/README.md
iindex 00885c5..9c73886 100644
---- a/README.md
++++ b/README.md
@@@ -522,6 +522,6 @@ Part of the [OpenVibe network](https://openvibe.network). Built in the open by [
<!-- versions:start -->
- openvibe-contracts: v0.209.0
--- openvibe-sdk: v0.60.0
++- openvibe-sdk: v0.61.0
- openvibe-shared: v4.7.0
<!-- versions:end -->
ddiff --git a/STATUS.json b/STATUS.json
iindex 16cfbd0..13f450c 100644
---- a/STATUS.json
++++ b/STATUS.json
@@@ -9,7 +9,7 @@
"domain": "openvibe.download and openvibe.video (openvibe.pics is parked: it redirects to openvibe.network)",
"packages": [
"openvibe-contracts v0.209.0",
-- "openvibe-sdk v0.60.0",
++ "openvibe-sdk v0.61.0",
"openvibe-shared v4.7.0"
],
"summary": "OpenVibe.MediaHub: openvibe.download as a private drive. Account-only uploads stream into OpenVibe.Media (a plain form up to 256 MB, resumable parts beyond). Files are private. Share links expire in 1 hour to 7 days, need a signed-in downloader and can be limited to named accounts (matched by subject). Reports suspend a share for staff. Downloads are always sandboxed attachments. Per-person quotas, and account export and deletion (ADR-033). openvibe.video serves public watch pages and playlists for Live's recordings and clips (no uploads); openvibe.pics is parked and redirects to openvibe.network.",
@@@ -43,7 +43,7 @@
"deploy files"
],
"contracts": "openvibe-contracts v0.209.0",
-- "sdk": "openvibe-sdk v0.60.0",
++ "sdk": "openvibe-sdk v0.61.0",
"shared": "openvibe-shared v4.7.0",
"deployment": "openvibe-ovh: unit openvibe-media-hub.service on 127.0.0.1:4990 (git checkout /opt/openvibe.download, env /etc/openvibe/media-hub.env, database ov_media_hub), deployed with `sudo ovhost deploy media-hub`; public at https://openvibe.download since 2026-10-08, https://openvibe.video since 2026-10-12 and https://openvibe.pics since 2026-10-15.",
"updated": "2026-10-21"
ddiff --git a/package-lock.json b/package-lock.json
iindex 2aee4e3..48c95b2 100644
---- a/package-lock.json
++++ b/package-lock.json
@@@ -16,7 +16,7 @@
"helmet": "^8.1.0",
"iovalkey": "^0.4.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.209.0",
-- "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.60.0",
++ "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.61.0",
"openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.7.0",
"pg": "^8.23.0",
"sharp": "^0.33.5"
@@@ -1351,9 +1351,9 @@
}
},
"node_modules/openvibe-sdk": {
-- "version": "0.60.0",
-- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.60.0",
-- "integrity": "sha512-hhgpGeEZsoGh+mGWcY22ZOXL+0vTNX+q7PtaAzxwNtr57K2XgCCHl2WqRpjHjrzyxPsjOOr9LBo3VYnmNv18bw==",
++ "version": "0.61.0",
++ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.61.0",
++ "integrity": "sha512-rhQKwM+yzVjhgUPTeqMOkL9N38fbSy8JYiEbJ8SwZk8LGcusde6LoQNhQ8UUxMJIwyY7xR1Rp5NVd6XRaVgTQQ==",
"license": "MIT",
"bin": {
"openvibe-download": "bin/openvibe-download.js"
ddiff --git a/package.json b/package.json
iindex 02c26bc..1c2f96d 100644
---- a/package.json
++++ b/package.json
@@@ -23,7 +23,7 @@
"helmet": "^8.1.0",
"iovalkey": "^0.4.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.209.0",
-- "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.60.0",
++ "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.61.0",
"openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.7.0",
"pg": "^8.23.0",
"sharp": "^0.33.5"
ddiff --git a/server/video/ai-edit.js b/server/video/ai-edit.js
iindex cf58ec0..756f5ac 100644
---- a/server/video/ai-edit.js
++++ b/server/video/ai-edit.js
@@@ -17,6 +17,9 @@
* forUpload(row) the AI row of an upload, parsed (or null)
*/
const { createServiceTokenClient } = require('openvibe-sdk/auth');
++// Public AI text never describes a person (the owner's rule): the overview is shown, indexed and offered as the
++// description, so it passes the shared filter when stored and when read (rows stored before).
++const { noLooks } = require('openvibe-sdk/no-looks');
const MAX_ATTEMPTS = 4;
const parse = (v, d) => { if (v == null) return d; if (typeof v !== 'string') return v; try { return JSON.parse(v); } catch { return d; } };
@@@ -104,7 +107,7 @@ function createAiEdit({ s, media, config, fetchImpl = globalThis.fetch, log = co
.map((x) => ({ start: Number(x.start), end: Number(x.end), text: clean(x.text, 500) })).slice(0, 5000);
return save(ai.upload_id, {
status: 'done', run_id: r.id || null, error: null, next_at: null,
-- overview: out.overview && out.overview.text ? clean(out.overview.text, 1500) : null,
++ overview: out.overview && out.overview.text ? (noLooks(clean(out.overview.text, 1500)).trim() || null) : null,
chapters: JSON.stringify(chaptersOf(out.highlights)), segments: JSON.stringify(segments),
highlights: JSON.stringify(highlightsOf(out.highlights)),
language: out.transcript.language ? clean(out.transcript.language, 8) : null,
@@@ -136,7 +139,7 @@ function createAiEdit({ s, media, config, fetchImpl = globalThis.fetch, log = co
enabled, sweep,
async forUpload(row) {
const ai = await s.db.maybe('SELECT * FROM video_upload_ai WHERE upload_id = $1', [row.id]);
-- return ai ? { ...ai, chapters: parse(ai.chapters, []), segments: parse(ai.segments, []), highlights: parse(ai.highlights, null) } : null;
++ return ai ? { ...ai, overview: ai.overview ? (noLooks(ai.overview).trim() || null) : ai.overview, chapters: parse(ai.chapters, []), segments: parse(ai.segments, []), highlights: parse(ai.highlights, null) } : null;
},
};
}
ddiff --git a/server/video/pages.js b/server/video/pages.js
iindex 3fdd839..725b3b2 100644
---- a/server/video/pages.js
++++ b/server/video/pages.js
@@@ -26,6 +26,8 @@ const ovServe = require('openvibe-shared/serve');
const { asyncRouter } = require('../http/router');
const { sameOrigin } = require('../http/principal');
const { html, raw, notice } = require('../render/html');
++// A recording's or clip's AI overview comes from Live, which filters it; read through the same filter here too.
++const { noLooks } = require('openvibe-sdk/no-looks');
const frame = require('openvibe-shared/frame');
const { send, asset, assetVersion, SITE_NAME } = require('./layout');
const { createLiveClient, LiveRefusedError } = require('./live-client');
@@@ -951,6 +953,7 @@ ${others.length ? html`<section class="ovv-section" aria-labelledby="h-other"><h
}
if (!data || !data.vod) return notFound(req, res);
const vod = data.vod;
++ if (vod && vod.ai_overview) vod.ai_overview = noLooks(vod.ai_overview).trim() || null;
const manageable = mayManage(req, 'vod', vod);
const clips = Array.isArray(data.clips) ? data.clips.filter((c) => c && c.visibility !== 'private') : [];
@@@ -1053,6 +1056,7 @@ ${clips.length ? html`<section class="ovv-panel" aria-labelledby="h-clips"><h2 i
}
if (!data || !data.clip) return notFound(req, res);
const clip = data.clip;
++ if (clip && clip.ai_overview) clip.ai_overview = noLooks(clip.ai_overview).trim() || null;
const manageable = mayManage(req, 'clip', clip);
// The comment thread, the playlist form and up next are asked together.
ddiff --git a/test/video-ai.test.js b/test/video-ai.test.js
iindex cf0c8a1..8441a68 100644
---- a/test/video-ai.test.js
++++ b/test/video-ai.test.js
@@@ -119,6 +119,14 @@ const OUTPUT = {
assert.deepStrictEqual([row.title, row.visibility], ['Coast', 'private'], 'a description-only change keeps the title and visibility');
});
++ await check('an overview that describes a person is read and shown without it (openvibe-sdk/no-looks)', async () => {
++ await t.ctx.s.db.query('UPDATE video_upload_ai SET overview = $1 WHERE upload_id = $2', ['A man in a red hoodie walks along the coast at sunset.', id]);
++ assert.strictEqual((await t.ctx.videoAi.forUpload({ id })).overview, 'A person walks along the coast at sunset.', 'a row stored before the rule reads clean');
++ const view = await vreq(t.base, `/u/${id}`, { cookie: kimCookie });
++ assert.ok(view.text.includes('A person walks along the coast at sunset.') && !view.text.includes('red hoodie'), 'the page shows the clean overview');
++ await t.ctx.s.db.query('UPDATE video_upload_ai SET overview = $1 WHERE upload_id = $2', [OUTPUT.overview.text, id]);
++ });
++
await check('a failure waits and retries with a new key; four attempts at most', async () => {
await t.ctx.s.db.query("UPDATE video_upload_ai SET status = 'queued', attempts = 0, next_at = NULL WHERE upload_id = $1", [id]);
mode = 'fail';