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)
Changes
@@@ -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 -->@@@ -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"@@@ -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"@@@ -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"@@@ -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 = coenabled, 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;},};}@@@ -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.@@@ -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';
@@ -522,6 +522,6 @@ Part of the [OpenVibe network](https://openvibe.network). Built in the open by [
@@ -522,6 +522,6 @@ Part of the [OpenVibe network](https://openvibe.network). Built in the open by [
<!-- versions:start -->
<!-- versions:start -->
- openvibe-contracts: v0.209.0
- openvibe-contracts: v0.209.0
-- openvibe-sdk: v0.60.0
+- openvibe-sdk: v0.61.0
- openvibe-shared: v4.7.0
- openvibe-shared: v4.7.0
<!-- versions:end -->
<!-- versions:end -->
@@ -9,7 +9,7 @@
@@ -9,7 +9,7 @@
"domain": "openvibe.download and openvibe.video (openvibe.pics is parked: it redirects to openvibe.network)",
"domain": "openvibe.download and openvibe.video (openvibe.pics is parked: it redirects to openvibe.network)",
"packages": [
"packages": [
"openvibe-contracts v0.209.0",
"openvibe-contracts v0.209.0",
- "openvibe-sdk v0.60.0",
+ "openvibe-sdk v0.61.0",
"openvibe-shared v4.7.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.",
"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 @@
@@ -43,7 +43,7 @@
"deploy files"
"deploy files"
],
],
"contracts": "openvibe-contracts v0.209.0",
"contracts": "openvibe-contracts v0.209.0",
- "sdk": "openvibe-sdk v0.60.0",
+ "sdk": "openvibe-sdk v0.61.0",
"shared": "openvibe-shared v4.7.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.",
"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"
"updated": "2026-10-21"
@@ -16,7 +16,7 @@
@@ -16,7 +16,7 @@
"helmet": "^8.1.0",
"helmet": "^8.1.0",
"iovalkey": "^0.4.0",
"iovalkey": "^0.4.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.209.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",
"openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.7.0",
"pg": "^8.23.0",
"pg": "^8.23.0",
"sharp": "^0.33.5"
"sharp": "^0.33.5"
@@ -1351,9 +1351,9 @@
@@ -1351,9 +1351,9 @@
}
}
},
},
"node_modules/openvibe-sdk": {
"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",
"license": "MIT",
"bin": {
"bin": {
"openvibe-download": "bin/openvibe-download.js"
"openvibe-download": "bin/openvibe-download.js"
@@ -23,7 +23,7 @@
@@ -23,7 +23,7 @@
"helmet": "^8.1.0",
"helmet": "^8.1.0",
"iovalkey": "^0.4.0",
"iovalkey": "^0.4.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.209.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",
"openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.7.0",
"pg": "^8.23.0",
"pg": "^8.23.0",
"sharp": "^0.33.5"
"sharp": "^0.33.5"
@@ -17,6 +17,9 @@
@@ -17,6 +17,9 @@
* forUpload(row) the AI row of an upload, parsed (or null)
* forUpload(row) the AI row of an upload, parsed (or null)
*/
*/
const { createServiceTokenClient } = require('openvibe-sdk/auth');
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 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; } };
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
@@ -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);
.map((x) => ({ start: Number(x.start), end: Number(x.end), text: clean(x.text, 500) })).slice(0, 5000);
return save(ai.upload_id, {
return save(ai.upload_id, {
status: 'done', run_id: r.id || null, error: null, next_at: null,
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),
chapters: JSON.stringify(chaptersOf(out.highlights)), segments: JSON.stringify(segments),
highlights: JSON.stringify(highlightsOf(out.highlights)),
highlights: JSON.stringify(highlightsOf(out.highlights)),
language: out.transcript.language ? clean(out.transcript.language, 8) : null,
language: out.transcript.language ? clean(out.transcript.language, 8) : null,
@@ -136,7 +139,7 @@ function createAiEdit({ s, media, config, fetchImpl = globalThis.fetch, log = co
@@ -136,7 +139,7 @@ function createAiEdit({ s, media, config, fetchImpl = globalThis.fetch, log = co
enabled, sweep,
enabled, sweep,
async forUpload(row) {
async forUpload(row) {
const ai = await s.db.maybe('SELECT * FROM video_upload_ai WHERE upload_id = $1', [row.id]);
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;
},
},
};
};
}
}
@@ -26,6 +26,8 @@ const ovServe = require('openvibe-shared/serve');
@@ -26,6 +26,8 @@ const ovServe = require('openvibe-shared/serve');
const { asyncRouter } = require('../http/router');
const { asyncRouter } = require('../http/router');
const { sameOrigin } = require('../http/principal');
const { sameOrigin } = require('../http/principal');
const { html, raw, notice } = require('../render/html');
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 frame = require('openvibe-shared/frame');
const { send, asset, assetVersion, SITE_NAME } = require('./layout');
const { send, asset, assetVersion, SITE_NAME } = require('./layout');
const { createLiveClient, LiveRefusedError } = require('./live-client');
const { createLiveClient, LiveRefusedError } = require('./live-client');
@@ -951,6 +953,7 @@ ${others.length ? html`<section class="ovv-section" aria-labelledby="h-other"><h
@@ -951,6 +953,7 @@ ${others.length ? html`<section class="ovv-section" aria-labelledby="h-other"><h
}
}
if (!data || !data.vod) return notFound(req, res);
if (!data || !data.vod) return notFound(req, res);
const vod = data.vod;
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 manageable = mayManage(req, 'vod', vod);
const clips = Array.isArray(data.clips) ? data.clips.filter((c) => c && c.visibility !== 'private') : [];
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
@@ -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);
if (!data || !data.clip) return notFound(req, res);
const clip = data.clip;
const clip = data.clip;
+ if (clip && clip.ai_overview) clip.ai_overview = noLooks(clip.ai_overview).trim() || null;
const manageable = mayManage(req, 'clip', clip);
const manageable = mayManage(req, 'clip', clip);
// The comment thread, the playlist form and up next are asked together.
// The comment thread, the playlist form and up next are asked together.
@@ -119,6 +119,14 @@ const OUTPUT = {
@@ -119,6 +119,14 @@ const OUTPUT = {
assert.deepStrictEqual([row.title, row.visibility], ['Coast', 'private'], 'a description-only change keeps the title and visibility');
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 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]);
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';
mode = 'fail';