openvibers/openvibe.ai Commits

Use free Openverse source for image type and date filters (#463)

OP OpenVibers committed 2 days ago · 325d6abd4a · 5 files, +21 −6

Changes

ddiff --git a/server/search/adapters/openverse.js b/server/search/adapters/openverse.js
iindex 823219c..4245c7b 100644
---- a/server/search/adapters/openverse.js
++++ b/server/search/adapters/openverse.js
@@@ -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,
}));
},
ddiff --git a/server/search/index.js b/server/search/index.js
iindex 2f48c19..9fc5872 100644
---- a/server/search/index.js
++++ b/server/search/index.js
@@@ -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;
ddiff --git a/server/search/openapi.js b/server/search/openapi.js
iindex efa2de0..ce17939 100644
---- a/server/search/openapi.js
++++ b/server/search/openapi.js
@@@ -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'] },
ddiff --git a/test/fixtures/search/openverse.json b/test/fixtures/search/openverse.json
iindex 32c900f..9211c1c 100644
---- a/test/fixtures/search/openverse.json
++++ b/test/fixtures/search/openverse.json
@@@ -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",
ddiff --git a/test/search-adapters.test.js b/test/search-adapters.test.js
iindex 0b66007..9e499d3 100644
---- a/test/search-adapters.test.js
++++ b/test/search-adapters.test.js
@@@ -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 }));