openvibers/openvibe.mediahub Commits
openvibe-shared v4.108.0 (was v4.107.4) (#283)
Changes
@@@ -525,5 +525,5 @@ Part of the [OpenVibe network](https://openvibe.network). Built in the open by [<!-- versions:start -->- openvibe-contracts: v0.222.0- openvibe-sdk: v0.82.0--- openvibe-shared: v4.107.4++- openvibe-shared: v4.108.0<!-- versions:end -->@@@ -10,7 +10,7 @@"packages": ["openvibe-contracts v0.222.0","openvibe-sdk v0.82.0",-- "openvibe-shared v4.107.4"++ "openvibe-shared v4.108.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.","works": [@@@ -44,7 +44,7 @@],"contracts": "openvibe-contracts v0.222.0","sdk": "openvibe-sdk v0.82.0",-- "shared": "openvibe-shared v4.107.4",++ "shared": "openvibe-shared v4.108.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-11-03"}@@@ -17,7 +17,7 @@"iovalkey": "^0.4.0","openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.222.0","openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.82.0",-- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.107.4",++ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.108.0","pg": "^8.23.0","sharp": "^0.33.5"},@@@ -1654,9 +1654,9 @@}},"node_modules/openvibe-shared": {-- "version": "4.107.4",-- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.107.4",-- "integrity": "sha512-tThN0m3KMncEHVcBOvE+ysHjsZTlVIDLG1P+/i2Mulmxfc/AUpebAXhpWWrut50OChZ8o+HeePxsSrPI8mUQOQ==",++ "version": "4.108.0",++ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.108.0",++ "integrity": "sha512-NPzwjxpzGIpFpIm4uw3+VwHfsxbJ2+cD9yuhRzSwolSwqv2kCmwaVtHJ0DA9gKO3K8PCBfIP3UpKzpuSbYEJ+g==","license": "MIT","dependencies": {"jsonwebtoken": "^9.0.0",@@@ -24,7 +24,7 @@"iovalkey": "^0.4.0","openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.222.0","openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.82.0",-- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.107.4",++ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.108.0","pg": "^8.23.0","sharp": "^0.33.5"},@@@ -79,6 +79,10 @@ function createDiscoveryRoutes(ctx) {r.get('/llms.txt', (_req, res) => {res.type('text/plain').set('Cache-Control', TEXT).send(seo.llmsTxt({name: SITE_NAME,++ // AGT-5: each of Download's own pages is linked at its Markdown twin (openvibe-shared 4.108.0 leaves files++ // and other sites' links as they are). What an agent may skip is in `optional`, written last.++ markdown: true,++ base: site,summary: 'OpenVibe.Download: a private drive on your OpenVibe account, with share links you control and folder backups.',details: 'Upload files to your own drive, share a file or folder with named OpenVibe accounts, anyone signed in, or a public download page (links expire and can be revoked), and keep a folder on your computer backed up with the openvibe-download command (a sync key from /sync). Every page is server-rendered and readable without JavaScript.',sections: [@@@ -88,14 +92,15 @@ function createDiscoveryRoutes(ctx) {{ title: 'What shipped on OpenVibe.MediaHub', url: abs('/updates') },] },{ title: 'Machine-readable', links: [-- { title: 'Sitemap', url: abs('/sitemap.xml') },-- { title: 'Full text for language models', url: abs('/llms-full.txt') },{ title: 'Release metadata (JSON)', url: abs('/release.json') },] },-- { title: 'Elsewhere', links: [-- { title: 'OpenVibe.Network', url: 'https://openvibe.network', note: 'accounts, apps and grants' },-- { title: 'OpenVibe.Services', url: 'https://openvibe.services', note: 'apps, keys and capability grants' },-- ] },++ ],++ // llmstxt.org's "## Optional": what an agent may skip when it needs a shorter context.++ optional: [++ { title: 'Sitemap', url: abs('/sitemap.xml') },++ { title: 'Full text for language models', url: abs('/llms-full.txt') },++ { title: 'OpenVibe.Network', url: 'https://openvibe.network', note: 'accounts, apps and grants' },++ { title: 'OpenVibe.Services', url: 'https://openvibe.services', note: 'apps, keys and capability grants' },],}));});@@@ -627,18 +627,26 @@ ${row.state === 'hidden' ? html`<button class="ovp-btn" name="action" value="res.send(seo.sitemapXml(['/', '/updates', ...legal.PATHS, ...POLICY_PATHS].map((p) => ({ loc: `${base}${p}` })))));// RFC 9116, canonical to this host (openvibe.pics).r.get('/.well-known/security.txt', securityTxt.handler(base));-- // The site as llms.txt describes it, shared by /llms.txt and /llms-full.txt (SEO-33).++ // The site as llms.txt describes it, shared by /llms.txt and /llms-full.txt (SEO-33). AGT-5: its own pages are++ // linked at their Markdown twins; `base` is this host's origin, so other sites' links keep theirs.const llmsSite = () => ({name: SITE_NAME,++ markdown: true,++ base,summary: 'Picture hosting on the OpenVibe network: upload a picture, get a link to share or embed, albums, any size or format.',details: 'Pictures are unlisted (reachable only by their link). Each one is re-encoded with its metadata removed, and can be had resized and converted at /i/<slug>/w/<320|640|1280|1920|2560>.<webp|avif|jpg|png>. Alt text: the uploader writes it or asks OpenVibe.AI for a draft (POST /api/v1/images/<slug>/describe). API: POST /api/v1/images with the picture as the body (an OpenVibe sign-in token as a Bearer).',sections: [-- { title: 'Start here', links: [{ title: SITE_NAME, url: `${base}/` }, { title: 'Terms', url: `${base}/terms` }, { title: 'DMCA', url: `${base}/dmca` }] },-- { title: 'Policies', links: [-- { title: 'What happens to your picture', url: `${base}/exif` },-- { title: 'What you may put here', url: `${base}/content-policy` },-- { title: 'Pictures and AI', url: `${base}/ai-training` },-- ] },++ { title: 'Start here', links: [{ title: SITE_NAME, url: `${base}/` }] },++ ],++ // llmstxt.org's "## Optional": what an agent may skip when it needs a shorter context.++ optional: [++ { title: 'What happens to your picture', url: `${base}/exif` },++ { title: 'What you may put here', url: `${base}/content-policy` },++ { title: 'Pictures and AI', url: `${base}/ai-training` },++ { title: 'Terms', url: `${base}/terms` },++ { title: 'DMCA', url: `${base}/dmca` },++ { title: 'Sitemap', url: `${base}/sitemap.xml`, note: 'only the home, the update log and the policy pages: no picture or album is ever listed' },++ { title: 'Full text for language models', url: `${base}/llms-full.txt` },],});// /.well-known/agent.json on the Pics host (SEO-2): its own manifest, not the drive's or the video host's.@@@ -653,6 +661,8 @@ ${row.state === 'hidden' ? html`<button class="ovp-btn" name="action" value="ressections: [{ title: 'About', pages: [{ title: d.name, url: `${base}/`, text: [d.summary, Array.isArray(d.details) ? d.details.join('\n') : d.details].filter(Boolean).join('\n\n') }] },...(d.sections || []).map((x) => ({ title: x.title, pages: (x.links || []).map((l) => ({ title: l.title, url: l.url, text: l.note || '' })) })),++ // AGT-5: the Optional links are still pages, so the full document names them too.++ ...((d.optional || []).length ? [{ title: 'Optional', pages: d.optional.map((l) => ({ title: l.title, url: l.url, text: l.note || '' })) }] : []),],}));});@@@ -1863,6 +1863,10 @@ ${isOwner ? html`<p class="ovv-fine">You can reorder items with the up and downconst links = (items, kind) => items.map((it) => ({ title: it.title || (kind === 'vod' ? 'Recording' : 'Clip'), url: `${base}/${kind === 'vod' ? 'v' : 'c'}/${it.id}`, note: String(it.description || it.ai_summary || it.summary || '').replace(/\s+/g, ' ').trim().slice(0, 600) }));return {name: SITE_NAME,++ // AGT-5: each of Video's own pages is linked at its Markdown twin (openvibe-shared 4.108.0 leaves files and++ // other sites' links as they are). Both /llms.txt and /llms-full.txt read this object.++ markdown: true,++ base,summary: 'Videos people upload or record in the browser, and stream recordings and clips from OpenVibe.Live, each with a watch page.',details: 'AI captions, chapters and a summary are made for uploads (OpenVibe.AI, on OpenVibe\'s own servers) and rendered on the watch pages, as are the AI chapters and captions of stream recordings. The AI cut makes an edited copy of an upload: the pauses cut out, or a highlights reel. Uploading needs a signed-in OpenVibe account; /upload and /record.',sections: [@@@ -1870,6 +1874,11 @@ ${isOwner ? html`<p class="ovv-fine">You can reorder items with the up and down{ title: 'Latest recordings', links: links(vodItems, 'vod') },{ title: 'Latest clips', links: links(clipItems, 'clip') },],++ // llmstxt.org's "## Optional": what an agent may skip when it needs a shorter context.++ optional: [++ { title: 'Sitemap', url: `${base}/sitemap.xml`, note: 'the watch pages Video is the canonical home of' },++ { title: 'Full text for language models', url: `${base}/llms-full.txt` },++ ],};}r.get('/llms.txt', async (req, res) => {@@@ -85,7 +85,10 @@ function onHost(base, p, host) {assert.match(r.headers.get('content-type'), /^text\/plain/);assert.match(r.headers.get('cache-control') || '', /public, max-age=\d+/);assert.ok(r.text.startsWith('# OpenVibe.Download'), 'no title');-- assert.ok(r.text.includes('(https://openvibe.download/updates)'), 'no updates link');++ // AGT-5: each of Download's own pages is linked at its Markdown twin, and the secondary links are Optional.++ assert.ok(r.text.includes('(https://openvibe.download/index.md)'), 'the home link is not its twin');++ assert.ok(r.text.includes('(https://openvibe.download/updates.md)'), 'no updates link');++ assert.ok(r.text.includes('\n## Optional\n'), 'no ## Optional section');assert.ok(r.text.includes('(https://openvibe.services)'), 'no pointer to the developer platform');assert.ok(r.text.includes('https://openvibe.download/sitemap.xml'), 'sitemap not listed');assert.ok(r.text.includes('(https://openvibe.download/llms-full.txt)'), 'llms-full.txt not listed');@@@ -102,7 +102,10 @@ const PAGES = ['/exif', '/content-policy', '/ai-training'];const map = (await t.get('/sitemap.xml', { headers: PICS })).text;for (const path of PAGES) assert.ok(map.includes(`<loc>https://openvibe.pics${path}</loc>`), `the sitemap lists ${path}`);const llms = (await t.get('/llms.txt', { headers: PICS })).text;-- for (const path of PAGES) assert.ok(llms.includes(`https://openvibe.pics${path}`), `llms.txt names ${path}`);++ // AGT-5: the site's own pages are linked at their Markdown twins, and the secondary links end in Optional.++ for (const path of PAGES) assert.ok(llms.includes(`https://openvibe.pics${path}.md`), `llms.txt names ${path}`);++ assert.ok(llms.includes('](https://openvibe.pics/index.md)'), 'the home link is not its twin');++ assert.ok(llms.includes('\n## Optional\n'), 'no ## Optional section');// SEO-2: the Pics host's own agent manifest.const agent = await t.get('/.well-known/agent.json', { headers: PICS });assert.strictEqual(agent.status, 200);@@@ -606,6 +606,9 @@ function countOccurrences(haystack, needle) {const llms = await getVideo('/llms.txt');assert.strictEqual(llms.status, 200);assert.ok(llms.text.includes('# OpenVibe.Video'));++ // AGT-5: Video's own pages are linked at their Markdown twins, and the secondary links end in Optional.++ assert.match(llms.text, /\]\(https:\/\/openvibe\.video\/index\.md\)/, 'the home link is not its twin');++ assert.ok(llms.text.includes('\n## Optional\n'), 'no ## Optional section');// SEO-33: the same, in full.const full = await getVideo('/llms-full.txt');assert.strictEqual(full.status, 200);
@@ -525,5 +525,5 @@ Part of the [OpenVibe network](https://openvibe.network). Built in the open by [
@@ -525,5 +525,5 @@ Part of the [OpenVibe network](https://openvibe.network). Built in the open by [
<!-- versions:start -->
<!-- versions:start -->
- openvibe-contracts: v0.222.0
- openvibe-contracts: v0.222.0
- openvibe-sdk: v0.82.0
- openvibe-sdk: v0.82.0
-- openvibe-shared: v4.107.4
+- openvibe-shared: v4.108.0
<!-- versions:end -->
<!-- versions:end -->
@@ -10,7 +10,7 @@
@@ -10,7 +10,7 @@
"packages": [
"packages": [
"openvibe-contracts v0.222.0",
"openvibe-contracts v0.222.0",
"openvibe-sdk v0.82.0",
"openvibe-sdk v0.82.0",
- "openvibe-shared v4.107.4"
+ "openvibe-shared v4.108.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.",
"works": [
"works": [
@@ -44,7 +44,7 @@
@@ -44,7 +44,7 @@
],
],
"contracts": "openvibe-contracts v0.222.0",
"contracts": "openvibe-contracts v0.222.0",
"sdk": "openvibe-sdk v0.82.0",
"sdk": "openvibe-sdk v0.82.0",
- "shared": "openvibe-shared v4.107.4",
+ "shared": "openvibe-shared v4.108.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-11-03"
"updated": "2026-11-03"
}
}
@@ -17,7 +17,7 @@
@@ -17,7 +17,7 @@
"iovalkey": "^0.4.0",
"iovalkey": "^0.4.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.222.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.222.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.82.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.82.0",
- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.107.4",
+ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.108.0",
"pg": "^8.23.0",
"pg": "^8.23.0",
"sharp": "^0.33.5"
"sharp": "^0.33.5"
},
},
@@ -1654,9 +1654,9 @@
@@ -1654,9 +1654,9 @@
}
}
},
},
"node_modules/openvibe-shared": {
"node_modules/openvibe-shared": {
- "version": "4.107.4",
- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.107.4",
- "integrity": "sha512-tThN0m3KMncEHVcBOvE+ysHjsZTlVIDLG1P+/i2Mulmxfc/AUpebAXhpWWrut50OChZ8o+HeePxsSrPI8mUQOQ==",
+ "version": "4.108.0",
+ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.108.0",
+ "integrity": "sha512-NPzwjxpzGIpFpIm4uw3+VwHfsxbJ2+cD9yuhRzSwolSwqv2kCmwaVtHJ0DA9gKO3K8PCBfIP3UpKzpuSbYEJ+g==",
"license": "MIT",
"license": "MIT",
"dependencies": {
"dependencies": {
"jsonwebtoken": "^9.0.0",
"jsonwebtoken": "^9.0.0",
@@ -24,7 +24,7 @@
@@ -24,7 +24,7 @@
"iovalkey": "^0.4.0",
"iovalkey": "^0.4.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.222.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.222.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.82.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.82.0",
- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.107.4",
+ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.108.0",
"pg": "^8.23.0",
"pg": "^8.23.0",
"sharp": "^0.33.5"
"sharp": "^0.33.5"
},
},
@@ -79,6 +79,10 @@ function createDiscoveryRoutes(ctx) {
@@ -79,6 +79,10 @@ function createDiscoveryRoutes(ctx) {
r.get('/llms.txt', (_req, res) => {
r.get('/llms.txt', (_req, res) => {
res.type('text/plain').set('Cache-Control', TEXT).send(seo.llmsTxt({
res.type('text/plain').set('Cache-Control', TEXT).send(seo.llmsTxt({
name: SITE_NAME,
name: SITE_NAME,
+ // AGT-5: each of Download's own pages is linked at its Markdown twin (openvibe-shared 4.108.0 leaves files
+ // and other sites' links as they are). What an agent may skip is in `optional`, written last.
+ markdown: true,
+ base: site,
summary: 'OpenVibe.Download: a private drive on your OpenVibe account, with share links you control and folder backups.',
summary: 'OpenVibe.Download: a private drive on your OpenVibe account, with share links you control and folder backups.',
details: 'Upload files to your own drive, share a file or folder with named OpenVibe accounts, anyone signed in, or a public download page (links expire and can be revoked), and keep a folder on your computer backed up with the openvibe-download command (a sync key from /sync). Every page is server-rendered and readable without JavaScript.',
details: 'Upload files to your own drive, share a file or folder with named OpenVibe accounts, anyone signed in, or a public download page (links expire and can be revoked), and keep a folder on your computer backed up with the openvibe-download command (a sync key from /sync). Every page is server-rendered and readable without JavaScript.',
sections: [
sections: [
@@ -88,14 +92,15 @@ function createDiscoveryRoutes(ctx) {
@@ -88,14 +92,15 @@ function createDiscoveryRoutes(ctx) {
{ title: 'What shipped on OpenVibe.MediaHub', url: abs('/updates') },
{ title: 'What shipped on OpenVibe.MediaHub', url: abs('/updates') },
] },
] },
{ title: 'Machine-readable', links: [
{ title: 'Machine-readable', links: [
- { title: 'Sitemap', url: abs('/sitemap.xml') },
- { title: 'Full text for language models', url: abs('/llms-full.txt') },
{ title: 'Release metadata (JSON)', url: abs('/release.json') },
{ title: 'Release metadata (JSON)', url: abs('/release.json') },
] },
] },
- { title: 'Elsewhere', links: [
- { title: 'OpenVibe.Network', url: 'https://openvibe.network', note: 'accounts, apps and grants' },
- { title: 'OpenVibe.Services', url: 'https://openvibe.services', note: 'apps, keys and capability grants' },
- ] },
+ ],
+ // llmstxt.org's "## Optional": what an agent may skip when it needs a shorter context.
+ optional: [
+ { title: 'Sitemap', url: abs('/sitemap.xml') },
+ { title: 'Full text for language models', url: abs('/llms-full.txt') },
+ { title: 'OpenVibe.Network', url: 'https://openvibe.network', note: 'accounts, apps and grants' },
+ { title: 'OpenVibe.Services', url: 'https://openvibe.services', note: 'apps, keys and capability grants' },
],
],
}));
}));
});
});
@@ -627,18 +627,26 @@ ${row.state === 'hidden' ? html`<button class="ovp-btn" name="action" value="res
@@ -627,18 +627,26 @@ ${row.state === 'hidden' ? html`<button class="ovp-btn" name="action" value="res
.send(seo.sitemapXml(['/', '/updates', ...legal.PATHS, ...POLICY_PATHS].map((p) => ({ loc: `${base}${p}` })))));
.send(seo.sitemapXml(['/', '/updates', ...legal.PATHS, ...POLICY_PATHS].map((p) => ({ loc: `${base}${p}` })))));
// RFC 9116, canonical to this host (openvibe.pics).
// RFC 9116, canonical to this host (openvibe.pics).
r.get('/.well-known/security.txt', securityTxt.handler(base));
r.get('/.well-known/security.txt', securityTxt.handler(base));
- // The site as llms.txt describes it, shared by /llms.txt and /llms-full.txt (SEO-33).
+ // The site as llms.txt describes it, shared by /llms.txt and /llms-full.txt (SEO-33). AGT-5: its own pages are
+ // linked at their Markdown twins; `base` is this host's origin, so other sites' links keep theirs.
const llmsSite = () => ({
const llmsSite = () => ({
name: SITE_NAME,
name: SITE_NAME,
+ markdown: true,
+ base,
summary: 'Picture hosting on the OpenVibe network: upload a picture, get a link to share or embed, albums, any size or format.',
summary: 'Picture hosting on the OpenVibe network: upload a picture, get a link to share or embed, albums, any size or format.',
details: 'Pictures are unlisted (reachable only by their link). Each one is re-encoded with its metadata removed, and can be had resized and converted at /i/<slug>/w/<320|640|1280|1920|2560>.<webp|avif|jpg|png>. Alt text: the uploader writes it or asks OpenVibe.AI for a draft (POST /api/v1/images/<slug>/describe). API: POST /api/v1/images with the picture as the body (an OpenVibe sign-in token as a Bearer).',
details: 'Pictures are unlisted (reachable only by their link). Each one is re-encoded with its metadata removed, and can be had resized and converted at /i/<slug>/w/<320|640|1280|1920|2560>.<webp|avif|jpg|png>. Alt text: the uploader writes it or asks OpenVibe.AI for a draft (POST /api/v1/images/<slug>/describe). API: POST /api/v1/images with the picture as the body (an OpenVibe sign-in token as a Bearer).',
sections: [
sections: [
- { title: 'Start here', links: [{ title: SITE_NAME, url: `${base}/` }, { title: 'Terms', url: `${base}/terms` }, { title: 'DMCA', url: `${base}/dmca` }] },
- { title: 'Policies', links: [
- { title: 'What happens to your picture', url: `${base}/exif` },
- { title: 'What you may put here', url: `${base}/content-policy` },
- { title: 'Pictures and AI', url: `${base}/ai-training` },
- ] },
+ { title: 'Start here', links: [{ title: SITE_NAME, url: `${base}/` }] },
+ ],
+ // llmstxt.org's "## Optional": what an agent may skip when it needs a shorter context.
+ optional: [
+ { title: 'What happens to your picture', url: `${base}/exif` },
+ { title: 'What you may put here', url: `${base}/content-policy` },
+ { title: 'Pictures and AI', url: `${base}/ai-training` },
+ { title: 'Terms', url: `${base}/terms` },
+ { title: 'DMCA', url: `${base}/dmca` },
+ { title: 'Sitemap', url: `${base}/sitemap.xml`, note: 'only the home, the update log and the policy pages: no picture or album is ever listed' },
+ { title: 'Full text for language models', url: `${base}/llms-full.txt` },
],
],
});
});
// /.well-known/agent.json on the Pics host (SEO-2): its own manifest, not the drive's or the video host's.
// /.well-known/agent.json on the Pics host (SEO-2): its own manifest, not the drive's or the video host's.
@@ -653,6 +661,8 @@ ${row.state === 'hidden' ? html`<button class="ovp-btn" name="action" value="res
@@ -653,6 +661,8 @@ ${row.state === 'hidden' ? html`<button class="ovp-btn" name="action" value="res
sections: [
sections: [
{ title: 'About', pages: [{ title: d.name, url: `${base}/`, text: [d.summary, Array.isArray(d.details) ? d.details.join('\n') : d.details].filter(Boolean).join('\n\n') }] },
{ title: 'About', pages: [{ title: d.name, url: `${base}/`, text: [d.summary, Array.isArray(d.details) ? d.details.join('\n') : d.details].filter(Boolean).join('\n\n') }] },
...(d.sections || []).map((x) => ({ title: x.title, pages: (x.links || []).map((l) => ({ title: l.title, url: l.url, text: l.note || '' })) })),
...(d.sections || []).map((x) => ({ title: x.title, pages: (x.links || []).map((l) => ({ title: l.title, url: l.url, text: l.note || '' })) })),
+ // AGT-5: the Optional links are still pages, so the full document names them too.
+ ...((d.optional || []).length ? [{ title: 'Optional', pages: d.optional.map((l) => ({ title: l.title, url: l.url, text: l.note || '' })) }] : []),
],
],
}));
}));
});
});
@@ -1863,6 +1863,10 @@ ${isOwner ? html`<p class="ovv-fine">You can reorder items with the up and down
@@ -1863,6 +1863,10 @@ ${isOwner ? html`<p class="ovv-fine">You can reorder items with the up and down
const links = (items, kind) => items.map((it) => ({ title: it.title || (kind === 'vod' ? 'Recording' : 'Clip'), url: `${base}/${kind === 'vod' ? 'v' : 'c'}/${it.id}`, note: String(it.description || it.ai_summary || it.summary || '').replace(/\s+/g, ' ').trim().slice(0, 600) }));
const links = (items, kind) => items.map((it) => ({ title: it.title || (kind === 'vod' ? 'Recording' : 'Clip'), url: `${base}/${kind === 'vod' ? 'v' : 'c'}/${it.id}`, note: String(it.description || it.ai_summary || it.summary || '').replace(/\s+/g, ' ').trim().slice(0, 600) }));
return {
return {
name: SITE_NAME,
name: SITE_NAME,
+ // AGT-5: each of Video's own pages is linked at its Markdown twin (openvibe-shared 4.108.0 leaves files and
+ // other sites' links as they are). Both /llms.txt and /llms-full.txt read this object.
+ markdown: true,
+ base,
summary: 'Videos people upload or record in the browser, and stream recordings and clips from OpenVibe.Live, each with a watch page.',
summary: 'Videos people upload or record in the browser, and stream recordings and clips from OpenVibe.Live, each with a watch page.',
details: 'AI captions, chapters and a summary are made for uploads (OpenVibe.AI, on OpenVibe\'s own servers) and rendered on the watch pages, as are the AI chapters and captions of stream recordings. The AI cut makes an edited copy of an upload: the pauses cut out, or a highlights reel. Uploading needs a signed-in OpenVibe account; /upload and /record.',
details: 'AI captions, chapters and a summary are made for uploads (OpenVibe.AI, on OpenVibe\'s own servers) and rendered on the watch pages, as are the AI chapters and captions of stream recordings. The AI cut makes an edited copy of an upload: the pauses cut out, or a highlights reel. Uploading needs a signed-in OpenVibe account; /upload and /record.',
sections: [
sections: [
@@ -1870,6 +1874,11 @@ ${isOwner ? html`<p class="ovv-fine">You can reorder items with the up and down
@@ -1870,6 +1874,11 @@ ${isOwner ? html`<p class="ovv-fine">You can reorder items with the up and down
{ title: 'Latest recordings', links: links(vodItems, 'vod') },
{ title: 'Latest recordings', links: links(vodItems, 'vod') },
{ title: 'Latest clips', links: links(clipItems, 'clip') },
{ title: 'Latest clips', links: links(clipItems, 'clip') },
],
],
+ // llmstxt.org's "## Optional": what an agent may skip when it needs a shorter context.
+ optional: [
+ { title: 'Sitemap', url: `${base}/sitemap.xml`, note: 'the watch pages Video is the canonical home of' },
+ { title: 'Full text for language models', url: `${base}/llms-full.txt` },
+ ],
};
};
}
}
r.get('/llms.txt', async (req, res) => {
r.get('/llms.txt', async (req, res) => {
@@ -85,7 +85,10 @@ function onHost(base, p, host) {
@@ -85,7 +85,10 @@ function onHost(base, p, host) {
assert.match(r.headers.get('content-type'), /^text\/plain/);
assert.match(r.headers.get('content-type'), /^text\/plain/);
assert.match(r.headers.get('cache-control') || '', /public, max-age=\d+/);
assert.match(r.headers.get('cache-control') || '', /public, max-age=\d+/);
assert.ok(r.text.startsWith('# OpenVibe.Download'), 'no title');
assert.ok(r.text.startsWith('# OpenVibe.Download'), 'no title');
- assert.ok(r.text.includes('(https://openvibe.download/updates)'), 'no updates link');
+ // AGT-5: each of Download's own pages is linked at its Markdown twin, and the secondary links are Optional.
+ assert.ok(r.text.includes('(https://openvibe.download/index.md)'), 'the home link is not its twin');
+ assert.ok(r.text.includes('(https://openvibe.download/updates.md)'), 'no updates link');
+ assert.ok(r.text.includes('\n## Optional\n'), 'no ## Optional section');
assert.ok(r.text.includes('(https://openvibe.services)'), 'no pointer to the developer platform');
assert.ok(r.text.includes('(https://openvibe.services)'), 'no pointer to the developer platform');
assert.ok(r.text.includes('https://openvibe.download/sitemap.xml'), 'sitemap not listed');
assert.ok(r.text.includes('https://openvibe.download/sitemap.xml'), 'sitemap not listed');
assert.ok(r.text.includes('(https://openvibe.download/llms-full.txt)'), 'llms-full.txt not listed');
assert.ok(r.text.includes('(https://openvibe.download/llms-full.txt)'), 'llms-full.txt not listed');
@@ -102,7 +102,10 @@ const PAGES = ['/exif', '/content-policy', '/ai-training'];
@@ -102,7 +102,10 @@ const PAGES = ['/exif', '/content-policy', '/ai-training'];
const map = (await t.get('/sitemap.xml', { headers: PICS })).text;
const map = (await t.get('/sitemap.xml', { headers: PICS })).text;
for (const path of PAGES) assert.ok(map.includes(`<loc>https://openvibe.pics${path}</loc>`), `the sitemap lists ${path}`);
for (const path of PAGES) assert.ok(map.includes(`<loc>https://openvibe.pics${path}</loc>`), `the sitemap lists ${path}`);
const llms = (await t.get('/llms.txt', { headers: PICS })).text;
const llms = (await t.get('/llms.txt', { headers: PICS })).text;
- for (const path of PAGES) assert.ok(llms.includes(`https://openvibe.pics${path}`), `llms.txt names ${path}`);
+ // AGT-5: the site's own pages are linked at their Markdown twins, and the secondary links end in Optional.
+ for (const path of PAGES) assert.ok(llms.includes(`https://openvibe.pics${path}.md`), `llms.txt names ${path}`);
+ assert.ok(llms.includes('](https://openvibe.pics/index.md)'), 'the home link is not its twin');
+ assert.ok(llms.includes('\n## Optional\n'), 'no ## Optional section');
// SEO-2: the Pics host's own agent manifest.
// SEO-2: the Pics host's own agent manifest.
const agent = await t.get('/.well-known/agent.json', { headers: PICS });
const agent = await t.get('/.well-known/agent.json', { headers: PICS });
assert.strictEqual(agent.status, 200);
assert.strictEqual(agent.status, 200);
@@ -606,6 +606,9 @@ function countOccurrences(haystack, needle) {
@@ -606,6 +606,9 @@ function countOccurrences(haystack, needle) {
const llms = await getVideo('/llms.txt');
const llms = await getVideo('/llms.txt');
assert.strictEqual(llms.status, 200);
assert.strictEqual(llms.status, 200);
assert.ok(llms.text.includes('# OpenVibe.Video'));
assert.ok(llms.text.includes('# OpenVibe.Video'));
+ // AGT-5: Video's own pages are linked at their Markdown twins, and the secondary links end in Optional.
+ assert.match(llms.text, /\]\(https:\/\/openvibe\.video\/index\.md\)/, 'the home link is not its twin');
+ assert.ok(llms.text.includes('\n## Optional\n'), 'no ## Optional section');
// SEO-33: the same, in full.
// SEO-33: the same, in full.
const full = await getVideo('/llms-full.txt');
const full = await getVideo('/llms-full.txt');
assert.strictEqual(full.status, 200);
assert.strictEqual(full.status, 200);