openvibers/openvibe.ai Commits
Use free Openverse source for image type and date filters (#463)
Changes
@@@ -29,6 +29,8 @@ module.exports = {: b[a],);if (b.commercial) q.set('license_type', 'commercial');++ if (b.type === 'photo') q.set('category', 'photograph');++ if (b.type === 'illustration') q.set('category', 'illustration');const out = await get('https://api.openverse.org/v1/images/?' + q);return (out.results || []).map((r) => ({title: r.title,@@@ -42,6 +44,8 @@ module.exports = {source_page: r.foreign_landing_url,attribution: r.attribution,format: r.filetype,++ type: r.category === 'photograph' ? 'photo' : r.category || null,++ indexed_on: r.indexed_on || null,published: null,}));},@@@ -58,10 +58,10 @@ function normalize(rows, engine, kind, b) {if (b.orientation) out = out.filter(r => r.width && r.height && (b.orientation === 'square' ? r.width === r.height : b.orientation === 'portrait' ? r.height > r.width : r.width > r.height));if (b.size) out = out.filter(r => r.width && r.height && (b.size === 'small' ? Math.max(r.width, r.height) < 800 : b.size === 'medium' ? Math.max(r.width, r.height) >= 800 && Math.max(r.width, r.height) < 2000 : Math.max(r.width, r.height) >= 2000));if (b.style && b.style !== 'any') out = out.filter(r => r.style === b.style);-- if (b.after) out = out.filter(r => r.published && Date.parse(r.published) >= Date.parse(b.after));++ if (b.after) out = out.filter(r => (r.published || r.indexed_on) && Date.parse(r.published || r.indexed_on) >= Date.parse(b.after));if (b.type === 'vector') out = out.filter(r => r.format === 'svg' || new URL(r.url).pathname.toLowerCase().endsWith('.svg'));-- if (b.type === 'photo') out = out.filter(r => ['unsplash', 'pexels', 'pixabay'].includes(engine));-- if (b.type === 'illustration') out = out.filter(r => !['unsplash', 'pexels'].includes(engine));++ if (b.type === 'photo') out = out.filter(r => ['unsplash', 'pexels', 'pixabay'].includes(engine) || r.type === 'photo');++ if (b.type === 'illustration') out = out.filter(r => r.type === 'illustration');return kind === 'images' ? rankImages(out, b.limit) : out.slice(0, b.limit);}function createSearch({ fetchImpl = guardedFetch, env = process.env, now = () => Date.now(), credentials = null, registry = null, targetGuard = publicTarget } = {}) {@@@ -99,8 +99,9 @@ function createSearch({ fetchImpl = guardedFetch, env = process.env, now = () =>if (!a.kinds.includes(kind) || b.engine && b.engine !== 'auto' && b.engine !== a.id ||b.freshness && !a.freshness.includes(b.freshness) || (a.requires || []).some(k => !env[k])) continue;if (kind === 'images' && b.color && a.id !== 'unsplash') continue;-- if (kind === 'images' && b.after && a.id !== 'unsplash') continue;-- if (kind === 'images' && b.type === 'photo' && !['unsplash', 'pexels', 'pixabay'].includes(a.id)) continue;++ if (kind === 'images' && b.after && !['unsplash', 'openverse'].includes(a.id)) continue;++ if (kind === 'images' && b.type === 'photo' && !['unsplash', 'pexels', 'pixabay', 'openverse'].includes(a.id)) continue;++ if (kind === 'images' && b.type === 'illustration' && a.id !== 'openverse') continue;if (kind === 'images' && b.type === 'vector' && !['openverse', 'commons'].includes(a.id)) continue;if (kind !== 'fetch' && a.max_query && [...b.q].length + (b.freshness ? 17 : 0) > a.max_query) continue;const own = a.byok && subject && credentials ? await credentials.searchForRun(subject, credentialId(a)) : null;@@@ -27,7 +27,7 @@ for (const kind of ['web', 'images', 'icons']) {orientation: { type: 'string', enum: ['landscape', 'portrait', 'square'] },size: { type: 'string', enum: ['small', 'medium', 'large'], description: 'Longest side: below 800px, 800–1999px, or 2000px and above. Missing dimensions do not match.' },color: { type: 'string', enum: ['black_and_white', 'black', 'white', 'yellow', 'orange', 'red', 'purple', 'magenta', 'green', 'teal', 'blue'], description: 'Available through Unsplash. Requires a configured Unsplash key.' },-- after: { type: 'string', format: 'date', description: 'Only results with a known publication date on or after this day. Available through Unsplash.' },++ after: { type: 'string', format: 'date', description: 'Only results on or after this day: original publication date where known, otherwise the Openverse indexing date. Results without a date are omitted.' },safe_search: { type: 'string', enum: ['strict'], default: 'strict' },type: { type: 'string', enum: ['icon', 'photo', 'illustration', 'vector'] }, set: { type: 'string' },style: { type: 'string', enum: ['any', 'line', 'solid'] },@@@ -7,6 +7,8 @@"thumbnail": "https://images.example.org/thumb.jpg","width": 2400,"height": 1600,++ "category": "photograph",++ "indexed_on": "2026-11-08T12:00:00Z","license": "by","creator": "Alex","foreign_landing_url": "https://example.org/article",@@@ -71,6 +71,14 @@ const json = value => new Response(JSON.stringify(value), { status: 200 });assert.equal(out.text, '# Lantern\nA new lantern');assert.equal(out.receipt.cost_usd, id === 'deepseek' ? .00075 : .0001);}++ let openverseUrl;++ const openverse = createSearch({ env: {}, fetchImpl: async (url) => { openverseUrl = new URL(url); return json(fixture('openverse').response); } });++ const photos = await openverse.search('images', { q: 'cat', engine: 'openverse', type: 'photo', after: '2026-11-01' });++ assert.equal(photos.results.length, 1);++ assert.equal(photos.results[0].indexed_on, '2026-11-08T12:00:00Z');++ assert.equal(openverseUrl.searchParams.get('category'), 'photograph');++ assert.equal((await openverse.search('images', { q: 'cat', engine: 'openverse', type: 'photo', after: '2026-12-01' })).results.length, 0);++ assert.equal((await openverse.search('images', { q: 'cat', engine: 'openverse', type: 'illustration' })).results.length, 0);// Explicit SSRF targets are rejected before a network connection, including remote readers.for (const url of ['http://localhost', 'https://127.0.0.1', 'https://[::1]', 'https://user:[email protected]']) {await assert.rejects(createSearch({ env: { SEARCH_JINA_KEY: KEY }, fetchImpl: () => assert.fail('unsafe call') }).read({ engine: 'jina', url }));
@@ -29,6 +29,8 @@ module.exports = {
@@ -29,6 +29,8 @@ module.exports = {
: b[a],
: b[a],
);
);
if (b.commercial) q.set('license_type', 'commercial');
if (b.commercial) q.set('license_type', 'commercial');
+ if (b.type === 'photo') q.set('category', 'photograph');
+ if (b.type === 'illustration') q.set('category', 'illustration');
const out = await get('https://api.openverse.org/v1/images/?' + q);
const out = await get('https://api.openverse.org/v1/images/?' + q);
return (out.results || []).map((r) => ({
return (out.results || []).map((r) => ({
title: r.title,
title: r.title,
@@ -42,6 +44,8 @@ module.exports = {
@@ -42,6 +44,8 @@ module.exports = {
source_page: r.foreign_landing_url,
source_page: r.foreign_landing_url,
attribution: r.attribution,
attribution: r.attribution,
format: r.filetype,
format: r.filetype,
+ type: r.category === 'photograph' ? 'photo' : r.category || null,
+ indexed_on: r.indexed_on || null,
published: null,
published: null,
}));
}));
},
},
@@ -58,10 +58,10 @@ function normalize(rows, engine, kind, b) {
@@ -58,10 +58,10 @@ function normalize(rows, engine, kind, b) {
if (b.orientation) out = out.filter(r => r.width && r.height && (b.orientation === 'square' ? r.width === r.height : b.orientation === 'portrait' ? r.height > r.width : r.width > r.height));
if (b.orientation) out = out.filter(r => r.width && r.height && (b.orientation === 'square' ? r.width === r.height : b.orientation === 'portrait' ? r.height > r.width : r.width > r.height));
if (b.size) out = out.filter(r => r.width && r.height && (b.size === 'small' ? Math.max(r.width, r.height) < 800 : b.size === 'medium' ? Math.max(r.width, r.height) >= 800 && Math.max(r.width, r.height) < 2000 : Math.max(r.width, r.height) >= 2000));
if (b.size) out = out.filter(r => r.width && r.height && (b.size === 'small' ? Math.max(r.width, r.height) < 800 : b.size === 'medium' ? Math.max(r.width, r.height) >= 800 && Math.max(r.width, r.height) < 2000 : Math.max(r.width, r.height) >= 2000));
if (b.style && b.style !== 'any') out = out.filter(r => r.style === b.style);
if (b.style && b.style !== 'any') out = out.filter(r => r.style === b.style);
- if (b.after) out = out.filter(r => r.published && Date.parse(r.published) >= Date.parse(b.after));
+ if (b.after) out = out.filter(r => (r.published || r.indexed_on) && Date.parse(r.published || r.indexed_on) >= Date.parse(b.after));
if (b.type === 'vector') out = out.filter(r => r.format === 'svg' || new URL(r.url).pathname.toLowerCase().endsWith('.svg'));
if (b.type === 'vector') out = out.filter(r => r.format === 'svg' || new URL(r.url).pathname.toLowerCase().endsWith('.svg'));
- if (b.type === 'photo') out = out.filter(r => ['unsplash', 'pexels', 'pixabay'].includes(engine));
- if (b.type === 'illustration') out = out.filter(r => !['unsplash', 'pexels'].includes(engine));
+ if (b.type === 'photo') out = out.filter(r => ['unsplash', 'pexels', 'pixabay'].includes(engine) || r.type === 'photo');
+ if (b.type === 'illustration') out = out.filter(r => r.type === 'illustration');
return kind === 'images' ? rankImages(out, b.limit) : out.slice(0, b.limit);
return kind === 'images' ? rankImages(out, b.limit) : out.slice(0, b.limit);
}
}
function createSearch({ fetchImpl = guardedFetch, env = process.env, now = () => Date.now(), credentials = null, registry = null, targetGuard = publicTarget } = {}) {
function createSearch({ fetchImpl = guardedFetch, env = process.env, now = () => Date.now(), credentials = null, registry = null, targetGuard = publicTarget } = {}) {
@@ -99,8 +99,9 @@ function createSearch({ fetchImpl = guardedFetch, env = process.env, now = () =>
@@ -99,8 +99,9 @@ function createSearch({ fetchImpl = guardedFetch, env = process.env, now = () =>
if (!a.kinds.includes(kind) || b.engine && b.engine !== 'auto' && b.engine !== a.id ||
if (!a.kinds.includes(kind) || b.engine && b.engine !== 'auto' && b.engine !== a.id ||
b.freshness && !a.freshness.includes(b.freshness) || (a.requires || []).some(k => !env[k])) continue;
b.freshness && !a.freshness.includes(b.freshness) || (a.requires || []).some(k => !env[k])) continue;
if (kind === 'images' && b.color && a.id !== 'unsplash') continue;
if (kind === 'images' && b.color && a.id !== 'unsplash') continue;
- if (kind === 'images' && b.after && a.id !== 'unsplash') continue;
- if (kind === 'images' && b.type === 'photo' && !['unsplash', 'pexels', 'pixabay'].includes(a.id)) continue;
+ if (kind === 'images' && b.after && !['unsplash', 'openverse'].includes(a.id)) continue;
+ if (kind === 'images' && b.type === 'photo' && !['unsplash', 'pexels', 'pixabay', 'openverse'].includes(a.id)) continue;
+ if (kind === 'images' && b.type === 'illustration' && a.id !== 'openverse') continue;
if (kind === 'images' && b.type === 'vector' && !['openverse', 'commons'].includes(a.id)) continue;
if (kind === 'images' && b.type === 'vector' && !['openverse', 'commons'].includes(a.id)) continue;
if (kind !== 'fetch' && a.max_query && [...b.q].length + (b.freshness ? 17 : 0) > a.max_query) continue;
if (kind !== 'fetch' && a.max_query && [...b.q].length + (b.freshness ? 17 : 0) > a.max_query) continue;
const own = a.byok && subject && credentials ? await credentials.searchForRun(subject, credentialId(a)) : null;
const own = a.byok && subject && credentials ? await credentials.searchForRun(subject, credentialId(a)) : null;
@@ -27,7 +27,7 @@ for (const kind of ['web', 'images', 'icons']) {
@@ -27,7 +27,7 @@ for (const kind of ['web', 'images', 'icons']) {
orientation: { type: 'string', enum: ['landscape', 'portrait', 'square'] },
orientation: { type: 'string', enum: ['landscape', 'portrait', 'square'] },
size: { type: 'string', enum: ['small', 'medium', 'large'], description: 'Longest side: below 800px, 800–1999px, or 2000px and above. Missing dimensions do not match.' },
size: { type: 'string', enum: ['small', 'medium', 'large'], description: 'Longest side: below 800px, 800–1999px, or 2000px and above. Missing dimensions do not match.' },
color: { type: 'string', enum: ['black_and_white', 'black', 'white', 'yellow', 'orange', 'red', 'purple', 'magenta', 'green', 'teal', 'blue'], description: 'Available through Unsplash. Requires a configured Unsplash key.' },
color: { type: 'string', enum: ['black_and_white', 'black', 'white', 'yellow', 'orange', 'red', 'purple', 'magenta', 'green', 'teal', 'blue'], description: 'Available through Unsplash. Requires a configured Unsplash key.' },
- after: { type: 'string', format: 'date', description: 'Only results with a known publication date on or after this day. Available through Unsplash.' },
+ after: { type: 'string', format: 'date', description: 'Only results on or after this day: original publication date where known, otherwise the Openverse indexing date. Results without a date are omitted.' },
safe_search: { type: 'string', enum: ['strict'], default: 'strict' },
safe_search: { type: 'string', enum: ['strict'], default: 'strict' },
type: { type: 'string', enum: ['icon', 'photo', 'illustration', 'vector'] }, set: { type: 'string' },
type: { type: 'string', enum: ['icon', 'photo', 'illustration', 'vector'] }, set: { type: 'string' },
style: { type: 'string', enum: ['any', 'line', 'solid'] },
style: { type: 'string', enum: ['any', 'line', 'solid'] },
@@ -7,6 +7,8 @@
@@ -7,6 +7,8 @@
"thumbnail": "https://images.example.org/thumb.jpg",
"thumbnail": "https://images.example.org/thumb.jpg",
"width": 2400,
"width": 2400,
"height": 1600,
"height": 1600,
+ "category": "photograph",
+ "indexed_on": "2026-11-08T12:00:00Z",
"license": "by",
"license": "by",
"creator": "Alex",
"creator": "Alex",
"foreign_landing_url": "https://example.org/article",
"foreign_landing_url": "https://example.org/article",
@@ -71,6 +71,14 @@ const json = value => new Response(JSON.stringify(value), { status: 200 });
@@ -71,6 +71,14 @@ const json = value => new Response(JSON.stringify(value), { status: 200 });
assert.equal(out.text, '# Lantern\nA new lantern');
assert.equal(out.text, '# Lantern\nA new lantern');
assert.equal(out.receipt.cost_usd, id === 'deepseek' ? .00075 : .0001);
assert.equal(out.receipt.cost_usd, id === 'deepseek' ? .00075 : .0001);
}
}
+ let openverseUrl;
+ const openverse = createSearch({ env: {}, fetchImpl: async (url) => { openverseUrl = new URL(url); return json(fixture('openverse').response); } });
+ const photos = await openverse.search('images', { q: 'cat', engine: 'openverse', type: 'photo', after: '2026-11-01' });
+ assert.equal(photos.results.length, 1);
+ assert.equal(photos.results[0].indexed_on, '2026-11-08T12:00:00Z');
+ assert.equal(openverseUrl.searchParams.get('category'), 'photograph');
+ assert.equal((await openverse.search('images', { q: 'cat', engine: 'openverse', type: 'photo', after: '2026-12-01' })).results.length, 0);
+ assert.equal((await openverse.search('images', { q: 'cat', engine: 'openverse', type: 'illustration' })).results.length, 0);
// Explicit SSRF targets are rejected before a network connection, including remote readers.
// Explicit SSRF targets are rejected before a network connection, including remote readers.
for (const url of ['http://localhost', 'https://127.0.0.1', 'https://[::1]', 'https://user:[email protected]']) {
for (const url of ['http://localhost', 'https://127.0.0.1', 'https://[::1]', 'https://user:[email protected]']) {
await assert.rejects(createSearch({ env: { SEARCH_JINA_KEY: KEY }, fetchImpl: () => assert.fail('unsafe call') }).read({ engine: 'jina', url }));
await assert.rejects(createSearch({ env: { SEARCH_JINA_KEY: KEY }, fetchImpl: () => assert.fail('unsafe call') }).read({ engine: 'jina', url }));