diff --git a/.gitignore b/.gitignore index bd15c35..324df4d 100644 --- a/.gitignore +++ b/.gitignore @@ -6,8 +6,10 @@ memory_graph.db memory_graph.db-journal .cache/ temp/ +tmp/ logs/ *.log +*.sqlite # Build & OS dist/ diff --git a/check_db.js b/check_db.js deleted file mode 100644 index 0a9ffdc..0000000 --- a/check_db.js +++ /dev/null @@ -1,12 +0,0 @@ -const Database = require('better-sqlite3'); -const path = require('path'); -const os = require('os'); -const dbPath = path.join(os.homedir(), 'AppData', 'Roaming', 'memory-desktop', 'db.sqlite'); - -try { - const db = new Database(dbPath, { readonly: true }); - const rows = db.prepare('SELECT path, ai_tags, face_count FROM media_items WHERE ai_tags IS NOT NULL LIMIT 20').all(); - console.log(JSON.stringify(rows, null, 2)); -} catch (err) { - console.error(err); -} diff --git a/check_db2.js b/check_db2.js deleted file mode 100644 index f83bd29..0000000 --- a/check_db2.js +++ /dev/null @@ -1,12 +0,0 @@ -const Database = require('better-sqlite3'); -const path = require('path'); -const os = require('os'); -const dbPath = path.join(process.env.APPDATA, 'memory-desktop', 'db.sqlite'); - -try { - const db = new Database(dbPath, { readonly: true }); - const rows = db.prepare('SELECT path, ai_tags, face_count FROM media_items WHERE ai_tags IS NOT NULL LIMIT 5').all(); - console.log('Database contents:', rows); -} catch (err) { - console.error(err); -} diff --git a/debug-db.js b/debug-db.js deleted file mode 100644 index 5a2a1c4..0000000 --- a/debug-db.js +++ /dev/null @@ -1,37 +0,0 @@ -const { app } = require('electron'); -const Database = require('better-sqlite3'); -const path = require('path'); -const fs = require('fs'); - -async function debug() { - console.log('--- DEBUG START ---'); - try { - const userData = process.env.APPDATA + '\\memory-desktop'; - console.log('UserData Path (approx):', userData); - - const dbPath = path.join(userData, 'memory-index.sqlite'); - console.log('DB Path:', dbPath); - - if (!fs.existsSync(dbPath)) { - console.log('DB FILE NOT FOUND!'); - return; - } - - const db = new Database(dbPath); - const tables = db.prepare("SELECT name FROM sqlite_master WHERE type='table'").all(); - console.log('Tables:', tables.map(t => t.name).join(', ')); - - if (tables.some(t => t.name === 'settings')) { - const settings = db.prepare("SELECT * FROM settings").all(); - console.log('Settings:', JSON.stringify(settings, null, 2)); - } else { - console.log('SETTINGS TABLE MISSING!'); - } - - } catch (err) { - console.error('Debug failed:', err); - } - console.log('--- DEBUG END ---'); -} - -debug(); diff --git a/docs/images/timeline_2.png b/docs/images/timeline_2.png index c7b4d33..5d9a982 100644 Binary files a/docs/images/timeline_2.png and b/docs/images/timeline_2.png differ diff --git a/lib/indexer.js b/lib/indexer.js index b1ae993..6685985 100644 --- a/lib/indexer.js +++ b/lib/indexer.js @@ -1,12 +1,10 @@ const { createDb } = require('./indexer/db'); const { runIndexing } = require('./indexer/index-service'); const { getEventsForRenderer, getIndexStats } = require('./indexer/repository'); -const { classifyImage } = require('./indexer/ai-service'); module.exports = { createDb, runIndexing, getEventsForRenderer, getIndexStats, - classifyImage, }; diff --git a/lib/indexer/repository.js b/lib/indexer/repository.js index 2489e1a..b62d225 100644 --- a/lib/indexer/repository.js +++ b/lib/indexer/repository.js @@ -24,10 +24,9 @@ function upsertMediaItems(db, files, runId) { last_seen_run = excluded.last_seen_run, is_missing = 0 `); - const markMissing = db.prepare('UPDATE media_items SET last_seen_run = -1 WHERE 0'); // placeholder - const realMarkMissing = db.prepare('UPDATE media_items SET is_missing = 1 WHERE last_seen_run < ?'); + const markMissing = db.prepare('UPDATE media_items SET is_missing = 1 WHERE last_seen_run <= ?'); const updateLastSeen = db.prepare('UPDATE media_items SET last_seen_run = ?, is_missing = 0 WHERE path = ?'); - return { selectByPath, upsert, markMissing: realMarkMissing, updateLastSeen }; + return { selectByPath, upsert, markMissing, updateLastSeen }; } function getActiveMediaItems(db) { diff --git a/lib/indexer/vector-search.js b/lib/indexer/vector-search.js index b0eba3f..e03782a 100644 --- a/lib/indexer/vector-search.js +++ b/lib/indexer/vector-search.js @@ -1,50 +1,71 @@ const { embedText } = require('./ai-service'); -function cosineSimilarity(bufA, bufB) { - if (!bufA || !bufB) return 0; - - const a = new Float32Array(bufA.buffer, bufA.byteOffset, bufA.byteLength / 4); - const b = new Float32Array(bufB.buffer, bufB.byteOffset, bufB.byteLength / 4); - - if (a.length !== b.length) return 0; - - let dotProduct = 0.0; - let normA = 0.0; - let normB = 0.0; - +const SIMILARITY_THRESHOLD = 0.22; +const MAX_RESULTS = 100; + +function toFloat32(src) { + if (!src) return null; + if (src instanceof Float32Array) return src; + if (Buffer.isBuffer(src) || src instanceof Uint8Array) { + if (src.byteLength < 4) return null; + return new Float32Array(src.buffer, src.byteOffset, src.byteLength / 4); + } + return null; +} + +function cosineSimilarity(a, b) { + if (!a || !b || a.length !== b.length) return 0; + let dot = 0, normA = 0, normB = 0; for (let i = 0; i < a.length; i++) { - dotProduct += a[i] * b[i]; + dot += a[i] * b[i]; normA += a[i] * a[i]; normB += b[i] * b[i]; } - if (normA === 0 || normB === 0) return 0; - return dotProduct / (Math.sqrt(normA) * Math.sqrt(normB)); + return dot / (Math.sqrt(normA) * Math.sqrt(normB)); } async function searchSemanticVectors(db, textQuery) { try { - console.log(`Embedding text query: "${textQuery}"`); const searchBuffer = await embedText(textQuery); if (!searchBuffer) return []; - console.log(`Executing Vector scan across SQLite...`); - // Pull all available embeddings from SQLite - const rows = db.prepare(`SELECT path, embedding FROM media_items WHERE embedding IS NOT NULL AND is_missing = 0`).all(); - - console.log(`Reticulating splines (Comparing ${rows.length} mathematical vectors).`); + const queryVec = toFloat32(searchBuffer); + if (!queryVec) return []; + const dims = queryVec.length; + + const rows = db.prepare( + 'SELECT path, embedding FROM media_items WHERE embedding IS NOT NULL AND is_missing = 0' + ).all(); + const results = []; - rows.forEach(row => { - const sim = cosineSimilarity(searchBuffer, row.embedding); - if (sim > 0.22) { // 22% similarity is a healthy semantic threshold for CLIP-ViT - results.push({ path: row.path, similarity: sim }); + let worstKept = -Infinity; + + for (let r = 0; r < rows.length; r++) { + const rowVec = toFloat32(rows[r].embedding); + if (!rowVec || rowVec.length !== dims) continue; + + const sim = cosineSimilarity(queryVec, rowVec); + if (sim <= SIMILARITY_THRESHOLD) continue; + + if (results.length < MAX_RESULTS) { + results.push({ path: rows[r].path, similarity: sim }); + } else { + if (sim <= worstKept) continue; + results.push({ path: rows[r].path, similarity: sim }); } - }); + + if (results.length >= MAX_RESULTS * 2) { + results.sort((a, b) => b.similarity - a.similarity); + results.length = MAX_RESULTS; + worstKept = results[MAX_RESULTS - 1].similarity; + } + } results.sort((a, b) => b.similarity - a.similarity); - - // Return max 100 paths - return results.slice(0, 100).map(r => r.path); + if (results.length > MAX_RESULTS) results.length = MAX_RESULTS; + + return results.map((r) => r.path); } catch (error) { console.error('Vector Search failed:', error); return []; diff --git a/package.json b/package.json index 352fc45..fae3c9e 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "memory-desktop", "version": "1.0.0", - "description": "", + "description": "AI-powered desktop photo gallery with semantic search, face recognition, and timeline clustering", "main": "main.js", "scripts": { "start": "electron .", @@ -10,8 +10,8 @@ "pack": "electron-builder --dir", "dist": "electron-builder" }, - "keywords": [], - "author": "", + "keywords": ["electron", "photo-gallery", "ai", "clip", "face-recognition", "semantic-search"], + "author": "Vishal", "license": "ISC", "type": "commonjs", "devDependencies": { @@ -41,6 +41,14 @@ "zip" ] }, + "mac": { + "target": [ + "dmg", + "zip" + ], + "category": "public.app-category.photography", + "darkModeSupport": true + }, "nsis": { "oneClick": true, "allowToChangeInstallationDirectory": false, @@ -48,10 +56,17 @@ "createStartMenuShortcut": true, "shortcutName": "Memory Gallery" }, + "dmg": { + "contents": [ + { "x": 130, "y": 220 }, + { "x": 410, "y": 220, "type": "link", "path": "/Applications" } + ] + }, "asar": true, "asarUnpack": [ + "**/node_modules/ffmpeg-static/ffmpeg", "**/node_modules/ffmpeg-static/ffmpeg.exe", - "**/node_modules/ffprobe-static/bin/win32/x64/ffprobe.exe" + "**/node_modules/ffprobe-static/bin/**/*" ], "files": [ "**/*", @@ -60,7 +75,8 @@ "!docs", "!.github", "!*.log", - "!.gitignore" + "!.gitignore", + "!tmp" ] } } diff --git a/preload.js b/preload.js deleted file mode 100644 index ff5f45d..0000000 --- a/preload.js +++ /dev/null @@ -1 +0,0 @@ -require('./src/preload'); \ No newline at end of file diff --git a/src/preload/index.js b/src/preload/index.js index f2685a0..9b0d661 100644 --- a/src/preload/index.js +++ b/src/preload/index.js @@ -1,9 +1,66 @@ const { contextBridge, ipcRenderer } = require('electron'); +const ALLOWED_INVOKE_CHANNELS = new Set([ + 'read-images', + 'refresh-library', + 'get-events', + 'search-semantic', + 'get-index-debug', + 'clear-cache', + 'get-index-roots', + 'set-index-roots', + 'get-people', + 'rename-person', + 'select-folder', +]); + +const ALLOWED_RECEIVE_CHANNELS = new Set([ + 'indexing-progress', + 'library-refresh-complete', + 'library-change-detected', + 'library-refresh-error', + 'visual-indexing-started', + 'visual-indexing-progress', + 'visual-indexing-complete', + 'face-indexing-started', + 'face-indexing-progress', + 'face-indexing-complete', + 'semantic-indexing-started', + 'semantic-indexing-progress', + 'semantic-indexing-complete', +]); + +const ALLOWED_SEND_CHANNELS = new Set([ + 'user-activity', +]); + const api = { - invoke: (channel, ...args) => ipcRenderer.invoke(channel, ...args), - on: (channel, callback) => ipcRenderer.on(channel, callback), - send: (channel, ...args) => ipcRenderer.send(channel, ...args), + invoke: async (channel, ...args) => { + if (!ALLOWED_INVOKE_CHANNELS.has(channel)) { + console.error(`[Preload] Blocked invoke on unknown channel: ${channel}`); + return null; + } + try { + return await ipcRenderer.invoke(channel, ...args); + } catch (error) { + console.error(`[IPC] ${channel} failed:`, error); + throw error; + } + }, + on: (channel, callback) => { + if (!ALLOWED_RECEIVE_CHANNELS.has(channel)) { + console.error(`[Preload] Blocked listener on unknown channel: ${channel}`); + return; + } + ipcRenderer.on(channel, callback); + }, + send: (channel, ...args) => { + if (!ALLOWED_SEND_CHANNELS.has(channel)) { + console.error(`[Preload] Blocked send on unknown channel: ${channel}`); + return; + } + ipcRenderer.send(channel, ...args); + }, readImages: () => ipcRenderer.invoke('read-images'), getIndexDebug: () => ipcRenderer.invoke('get-index-debug'), }; diff --git a/src/renderer/app.js b/src/renderer/app.js index a6e799e..3c26fed 100644 --- a/src/renderer/app.js +++ b/src/renderer/app.js @@ -1631,16 +1631,6 @@ console.error('Failed to open folders:', err); alert('Could not open folder settings.'); } - try { - const settings = await window.api.invoke('get-index-roots'); - state.indexRoots = Array.isArray(settings) ? settings : settings.roots; - ui.includeVideosCheckbox.checked = settings.includeVideos !== false; - renderRootsList(); - ui.settingsModal.classList.remove('hidden'); - } catch (err) { - console.error('Failed to open folders:', err); - alert('Could not open folder settings.'); - } }; ui.closeSettingsBtn.onclick = () => ui.settingsModal.classList.add('hidden'); diff --git a/src/renderer/styles.css b/src/renderer/styles.css index dce9fe2..539cd67 100644 --- a/src/renderer/styles.css +++ b/src/renderer/styles.css @@ -462,10 +462,6 @@ button:active { transform: translateY(0); } -button:active { - transform: translateY(0); -} - #status { position: fixed; bottom: 2rem; diff --git a/test-ai.js b/test-ai.js deleted file mode 100644 index 0701984..0000000 --- a/test-ai.js +++ /dev/null @@ -1,42 +0,0 @@ -const { pipeline, env } = require('@xenova/transformers'); -const path = require('path'); -const fs = require('fs'); - -env.allowLocalModels = false; -env.useBrowserCache = false; - -async function test() { - const filePath = process.argv[2]; - if (!filePath) { - console.error('Please provide a file path'); - return; - } - - console.log('Testing models on:', filePath); - - try { - console.log('Loading detector...'); - const detector = await pipeline('object-detection', 'Xenova/blazeface', { quantized: true }); - console.log('Detector loaded.'); - - console.log('Detecting...'); - const detections = await detector(filePath, { threshold: 0.1 }); - console.log('Detections:', JSON.stringify(detections, null, 2)); - - if (detections.length > 0) { - console.log('Loading embedder...'); - const embedder = await pipeline('image-feature-extraction', 'Xenova/facenet-base', { quantized: true }); - console.log('Embedder loaded.'); - - console.log('Extracting embedding...'); - const result = await embedder(filePath); - console.log('Embedding data length:', result.data.length); - } else { - console.log('No faces detected at threshold 0.1'); - } - } catch (err) { - console.error('TEST FAILED:', err); - } -} - -test(); diff --git a/test/basic.test.js b/test/basic.test.js index 654e9a7..5fb4108 100644 --- a/test/basic.test.js +++ b/test/basic.test.js @@ -1,19 +1,332 @@ const assert = require('assert'); +const path = require('path'); +const fs = require('fs'); -// Simple sanity test for CI -console.log('Running basic sanity tests...'); +let passed = 0; +let failed = 0; +let skipped = 0; -try { - // Check if core modules can be required +function test(name, fn) { + try { + fn(); + passed++; + console.log(` ✓ ${name}`); + } catch (error) { + failed++; + console.error(` ✗ ${name}`); + console.error(` ${error.message}`); + } +} + +function skipTest(name, reason) { + skipped++; + console.log(` ○ ${name} (skipped: ${reason})`); +} + +let hasSqlite = false; +try { require('better-sqlite3'); hasSqlite = true; } catch (_) {} + +let hasElectron = false; +try { require('electron'); hasElectron = true; } catch (_) {} + +// --------------------------------------------------------------------------- +// Module import checks +// --------------------------------------------------------------------------- +console.log('\n— Module imports —'); + +if (hasSqlite) { + test('lib/indexer exports expected functions', () => { const indexer = require('../lib/indexer'); - assert.strictEqual(typeof indexer.createDb, 'function', 'createDb should be a function'); - console.log('✓ Successfully required lib/indexer and verified createDb function.'); - - // You can add more basic checks here - - console.log('All tests passed!'); - process.exit(0); -} catch (error) { - console.error('Test failed:', error); - process.exit(1); + assert.strictEqual(typeof indexer.createDb, 'function'); + assert.strictEqual(typeof indexer.runIndexing, 'function'); + assert.strictEqual(typeof indexer.getEventsForRenderer, 'function'); + assert.strictEqual(typeof indexer.getIndexStats, 'function'); + }); +} else { + skipTest('lib/indexer exports expected functions', 'better-sqlite3 not available'); +} + +test('lib/indexer/constants exports correct values', () => { + const c = require('../lib/indexer/constants'); + assert.ok(c.SUPPORTED_MEDIA instanceof Set); + assert.ok(c.SUPPORTED_MEDIA.has('.jpg')); + assert.ok(c.SUPPORTED_MEDIA.has('.mp4')); + assert.ok(c.VIDEO_EXTENSIONS instanceof Set); + assert.ok(c.VIDEO_EXTENSIONS.has('.mp4')); + assert.ok(!c.VIDEO_EXTENSIONS.has('.jpg')); + assert.strictEqual(c.TWO_HOURS_MS, 7200000); + assert.strictEqual(typeof c.MAX_CLUSTER_SIZE, 'number'); + assert.strictEqual(typeof c.LOCATION_SPLIT_DISTANCE_KM, 'number'); +}); + +// --------------------------------------------------------------------------- +// Database creation +// --------------------------------------------------------------------------- +console.log('\n— Database —'); + +const TEST_DB_PATH = path.join(__dirname, '_test_temp.sqlite'); +let createTestDb, cleanupDb; + +if (hasSqlite) { + createTestDb = () => { + if (fs.existsSync(TEST_DB_PATH)) fs.unlinkSync(TEST_DB_PATH); + const { createDb } = require('../lib/indexer/db'); + return createDb(TEST_DB_PATH); + }; + cleanupDb = () => { try { fs.unlinkSync(TEST_DB_PATH); } catch (_) {} }; + + test('createDb creates all expected tables', () => { + const db = createTestDb(); + const tables = db.prepare("SELECT name FROM sqlite_master WHERE type='table'").all().map(t => t.name); + ['media_items', 'events', 'event_items', 'geocoding_cache', 'people', 'media_faces', 'settings'].forEach(t => { + assert.ok(tables.includes(t), `missing table: ${t}`); + }); + db.close(); + cleanupDb(); + }); + + test('createDb enables WAL mode', () => { + const db = createTestDb(); + const mode = db.pragma('journal_mode', { simple: true }); + assert.strictEqual(mode, 'wal'); + db.close(); + cleanupDb(); + }); +} else { + createTestDb = () => null; + cleanupDb = () => {}; + skipTest('createDb creates all expected tables', 'better-sqlite3 not available'); + skipTest('createDb enables WAL mode', 'better-sqlite3 not available'); } + +// --------------------------------------------------------------------------- +// Clustering +// --------------------------------------------------------------------------- +console.log('\n— Clustering —'); + +const { buildEvents } = require('../lib/indexer/cluster'); + +test('buildEvents returns empty array for empty input', () => { + const result = buildEvents([]); + assert.ok(Array.isArray(result)); + assert.strictEqual(result.length, 0); +}); + +test('buildEvents groups items within time window into one event', () => { + const base = Date.now(); + const items = [ + { id: 1, path: '/a.jpg', resolved_time_ms: base }, + { id: 2, path: '/b.jpg', resolved_time_ms: base + 60000 }, + { id: 3, path: '/c.jpg', resolved_time_ms: base + 120000 }, + ]; + const events = buildEvents(items); + assert.strictEqual(events.length, 1); + assert.strictEqual(events[0].items.length, 3); + assert.strictEqual(events[0].startTimeMs, base); + assert.strictEqual(events[0].endTimeMs, base + 120000); +}); + +test('buildEvents splits on time gap > 2 hours', () => { + const base = Date.now(); + const TWO_HOURS = 2 * 60 * 60 * 1000; + const items = [ + { id: 1, path: '/a.jpg', resolved_time_ms: base }, + { id: 2, path: '/b.jpg', resolved_time_ms: base + TWO_HOURS + 1 }, + ]; + const events = buildEvents(items); + assert.strictEqual(events.length, 2); + assert.strictEqual(events[0].items.length, 1); + assert.strictEqual(events[1].items.length, 1); +}); + +test('buildEvents splits on large location gap', () => { + const base = Date.now(); + const items = [ + { id: 1, path: '/a.jpg', resolved_time_ms: base, latitude: 40.7128, longitude: -74.0060 }, + { id: 2, path: '/b.jpg', resolved_time_ms: base + 1000, latitude: 48.8566, longitude: 2.3522 }, + ]; + const events = buildEvents(items); + assert.strictEqual(events.length, 2); +}); + +test('buildEvents keeps nearby items in same cluster', () => { + const base = Date.now(); + const items = [ + { id: 1, path: '/a.jpg', resolved_time_ms: base, latitude: 40.7128, longitude: -74.0060 }, + { id: 2, path: '/b.jpg', resolved_time_ms: base + 1000, latitude: 40.7130, longitude: -74.0058 }, + ]; + const events = buildEvents(items); + assert.strictEqual(events.length, 1); +}); + +test('buildEvents generates stable IDs for same input', () => { + const base = Date.now(); + const items = [ + { id: 1, path: '/a.jpg', resolved_time_ms: base }, + { id: 2, path: '/b.jpg', resolved_time_ms: base + 1000 }, + ]; + const e1 = buildEvents(items); + const e2 = buildEvents(items); + assert.strictEqual(e1[0].id, e2[0].id); +}); + +test('buildEvents computes location center', () => { + const base = Date.now(); + const items = [ + { id: 1, path: '/a.jpg', resolved_time_ms: base, latitude: 40.0, longitude: -74.0, place_name: 'NYC' }, + { id: 2, path: '/b.jpg', resolved_time_ms: base + 1000, latitude: 40.0, longitude: -74.0, place_name: 'NYC' }, + ]; + const events = buildEvents(items); + assert.strictEqual(typeof events[0].centerLat, 'number'); + assert.strictEqual(typeof events[0].centerLon, 'number'); + assert.ok(Math.abs(events[0].centerLat - 40.0) < 0.001); + assert.strictEqual(events[0].placeName, 'NYC'); +}); + +// --------------------------------------------------------------------------- +// Repository +// --------------------------------------------------------------------------- +console.log('\n— Repository —'); + +if (hasSqlite) { + const { + upsertMediaItems, + getActiveMediaItems, + replaceEvents, + getEventsForRenderer, + getIndexStats, + getPeople, + } = require('../lib/indexer/repository'); + + test('upsertMediaItems + getActiveMediaItems round-trip', () => { + const db = createTestDb(); + const queries = upsertMediaItems(db, [], 1); + + queries.upsert.run({ + path: '/test/photo.jpg', + ext: '.jpg', + mediaType: 'image', + size: 1024, + mtimeMs: Date.now(), + resolvedTimeMs: Date.now(), + resolvedSource: 'exif', + latitude: null, + longitude: null, + locationSource: null, + placeName: null, + aiTags: null, + faceCount: 0, + embedding: null, + thumbnailPath: null, + facesIndexed: 0, + visualIndexed: 0, + confidence: 1.0, + lastSeenRun: 1, + }); + + const active = getActiveMediaItems(db); + assert.strictEqual(active.length, 1); + assert.strictEqual(active[0].path, '/test/photo.jpg'); + + db.close(); + cleanupDb(); + }); + + test('replaceEvents + getEventsForRenderer round-trip', () => { + const db = createTestDb(); + const queries = upsertMediaItems(db, [], 1); + + queries.upsert.run({ + path: '/test/a.jpg', ext: '.jpg', mediaType: 'image', size: 512, + mtimeMs: Date.now(), resolvedTimeMs: Date.now(), resolvedSource: 'mtime', + latitude: null, longitude: null, locationSource: null, placeName: null, + aiTags: 'dog, park', faceCount: 0, embedding: null, thumbnailPath: null, + facesIndexed: 0, visualIndexed: 1, confidence: 0.8, lastSeenRun: 1, + }); + + const active = getActiveMediaItems(db); + const events = buildEvents(active); + replaceEvents(db, events); + + const rendered = getEventsForRenderer(db, 'date'); + assert.strictEqual(rendered.length, 1); + assert.strictEqual(rendered[0].items.length, 1); + assert.strictEqual(rendered[0].items[0].aiTags, 'dog, park'); + + db.close(); + cleanupDb(); + }); + + test('getIndexStats returns correct counts', () => { + const db = createTestDb(); + const queries = upsertMediaItems(db, [], 1); + queries.upsert.run({ + path: '/x.jpg', ext: '.jpg', mediaType: 'image', size: 100, + mtimeMs: Date.now(), resolvedTimeMs: Date.now(), resolvedSource: 'mtime', + latitude: 10.0, longitude: 20.0, locationSource: 'exif', placeName: null, + aiTags: null, faceCount: 0, embedding: null, thumbnailPath: null, + facesIndexed: 0, visualIndexed: 0, confidence: 1.0, lastSeenRun: 1, + }); + + const stats = getIndexStats(db); + assert.strictEqual(stats.activeMedia, 1); + assert.strictEqual(stats.missingMedia, 0); + assert.strictEqual(stats.geotaggedMedia, 1); + + db.close(); + cleanupDb(); + }); + + test('getPeople returns empty array on fresh DB', () => { + const db = createTestDb(); + const people = getPeople(db); + assert.ok(Array.isArray(people)); + assert.strictEqual(people.length, 0); + db.close(); + cleanupDb(); + }); +} else { + skipTest('upsertMediaItems + getActiveMediaItems round-trip', 'better-sqlite3 not available'); + skipTest('replaceEvents + getEventsForRenderer round-trip', 'better-sqlite3 not available'); + skipTest('getIndexStats returns correct counts', 'better-sqlite3 not available'); + skipTest('getPeople returns empty array on fresh DB', 'better-sqlite3 not available'); +} + +// --------------------------------------------------------------------------- +// Scanner +// --------------------------------------------------------------------------- +console.log('\n— Scanner —'); + +const { getMediaFileRecord } = require('../lib/indexer/scanner'); + +test('getMediaFileRecord returns null for non-existent file', () => { + const result = getMediaFileRecord('/definitely/not/a/real/file.jpg'); + assert.strictEqual(result, null); +}); + +test('getMediaFileRecord returns null for unsupported extension', () => { + const result = getMediaFileRecord(__filename); + assert.strictEqual(result, null); +}); + +// --------------------------------------------------------------------------- +// Vector search helpers +// --------------------------------------------------------------------------- +console.log('\n— Vector search —'); + +if (hasElectron) { + test('vector-search module exports searchSemanticVectors', () => { + const vs = require('../lib/indexer/vector-search'); + assert.strictEqual(typeof vs.searchSemanticVectors, 'function'); + }); +} else { + skipTest('vector-search module exports searchSemanticVectors', 'electron not available'); +} + +// --------------------------------------------------------------------------- +// Summary +// --------------------------------------------------------------------------- +const total = passed + failed + skipped; +console.log(`\n${total} tests: ${passed} passed, ${failed} failed, ${skipped} skipped\n`); +process.exit(failed > 0 ? 1 : 0); + \ No newline at end of file diff --git a/test_ai.js b/test_ai.js deleted file mode 100644 index 8065e91..0000000 --- a/test_ai.js +++ /dev/null @@ -1,16 +0,0 @@ -const { analyzeMedia } = require('./lib/indexer/ai-service'); -const path = require('path'); -const fs = require('fs'); - -async function run() { - const imagesDir = path.join(require('os').homedir(), 'Pictures'); - const files = fs.readdirSync(imagesDir).filter(f => f.endsWith('.jpg') || f.endsWith('.png')); - if (files.length > 0) { - console.log('Testing image:', files[0]); - const res = await analyzeMedia(path.join(imagesDir, files[0])); - console.log('Result:', res); - } else { - console.log('No images found in Pictures'); - } -} -run(); diff --git a/test_clip.js b/test_clip.js deleted file mode 100644 index 6dd0887..0000000 --- a/test_clip.js +++ /dev/null @@ -1,22 +0,0 @@ -const { pipeline, env } = require('@xenova/transformers'); - -// Prevent downloading to system cache, use local model to ensure privacy -env.localModelPath = './models'; -env.backends.onnx.wasm.numThreads = 1; - -async function testClip() { - console.log('Loading text extractor...'); - const textExtractor = await pipeline('feature-extraction', 'Xenova/clip-vit-base-patch32'); - console.log('Text loaded! Computing "dog"...'); - const textRes = await textExtractor("a photo of a dog", { pooling: 'mean', normalize: true }); - console.log('Text Embed shape:', textRes.dims); // Should be [1, 512] - - console.log('Loading vision extractor...'); - const imageExtractor = await pipeline('image-feature-extraction', 'Xenova/clip-vit-base-patch32'); - const imgUrl = "https://images.unsplash.com/photo-1543852786-1cf6624b9987"; // Cat image - console.log('Downloading and computing image...', imgUrl); - const imgRes = await imageExtractor(imgUrl); - console.log('Image Embed shape:', imgRes.dims); // Should be [1, 512] -} - -testClip().catch(console.error); diff --git a/tmp/check_db.js b/tmp/check_db.js deleted file mode 100644 index c58f525..0000000 --- a/tmp/check_db.js +++ /dev/null @@ -1,14 +0,0 @@ -const Database = require('better-sqlite3'); -const path = require('path'); -const os = require('os'); - -const dbPath = path.join(os.homedir(), '.memory-desktop', 'index.db'); -const db = new Database(dbPath); - -const stmt = db.prepare('SELECT path, ai_tags FROM media_items'); -const rows = stmt.all(); - -console.log('--- AI Tags in Database ---'); -rows.forEach(row => { - console.log(`${path.basename(row.path)}: ${row.ai_tags}`); -}); diff --git a/tmp/clear_tags.js b/tmp/clear_tags.js deleted file mode 100644 index 5cf3e47..0000000 --- a/tmp/clear_tags.js +++ /dev/null @@ -1,16 +0,0 @@ -const { app } = require('electron'); -const Database = require('better-sqlite3'); -const path = require('path'); -const os = require('os'); - -app.whenReady().then(() => { - try { - const dbPath = path.join(os.homedir(), '.memory-desktop', 'index.db'); - const db = new Database(dbPath); - const info = db.prepare('UPDATE media_items SET ai_tags = NULL').run(); - console.log(`Cleared old AI tags for ${info.changes} images!`); - } catch (err) { - console.error('Failed to clear tags:', err); - } - app.quit(); -}); diff --git a/tmp/debug_net.js b/tmp/debug_net.js deleted file mode 100644 index 5c2b39b..0000000 --- a/tmp/debug_net.js +++ /dev/null @@ -1,34 +0,0 @@ -const https = require('https'); - -const urls = [ - 'https://huggingface.co/Xenova/mobilenet_v1_1.0_224/resolve/main/config.json', - 'https://huggingface.co/Xenova/mobilenet_v1_1.0_224/resolve/main/preprocessor_config.json' -]; - -async function testUrl(url) { - return new Promise((resolve) => { - console.log(`Testing: ${url}`); - const req = https.get(url, { - headers: { - 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36' - } - }, (res) => { - console.log(`Status: ${res.statusCode}`); - console.log(`Headers: ${JSON.stringify(res.headers, null, 2)}`); - resolve(res.statusCode); - }); - - req.on('error', (err) => { - console.error(`Error: ${err.message}`); - resolve(500); - }); - }); -} - -async function run() { - for (const url of urls) { - await testUrl(url); - } -} - -run(); diff --git a/tmp/test-memory.sqlite b/tmp/test-memory.sqlite deleted file mode 100644 index 9cc0e45..0000000 Binary files a/tmp/test-memory.sqlite and /dev/null differ diff --git a/tmp/test_ai.js b/tmp/test_ai.js deleted file mode 100644 index e50bc0a..0000000 --- a/tmp/test_ai.js +++ /dev/null @@ -1,20 +0,0 @@ -const { classifyImage } = require('../lib/indexer/ai-service'); -const path = require('path'); - -async function test() { - console.log('--- Testing AI Classification ---'); - // Use any image in the repo if possible, or just a dummy path to trigger model download - // We'll use the path from the user's error to be sure - const testPath = 'C:\\Users\\Vishal\\Pictures\\car.jpg'; - - console.log(`Classifying: ${testPath}`); - try { - const tags = await classifyImage(testPath); - console.log(`SUCCESS: Tags generated: ${tags}`); - } catch (err) { - console.error(`FAILED: ${err.message}`); - } - process.exit(0); -} - -test(); diff --git a/tmp/test_geocoder.js b/tmp/test_geocoder.js deleted file mode 100644 index f5d7f91..0000000 --- a/tmp/test_geocoder.js +++ /dev/null @@ -1,47 +0,0 @@ -const Database = require('better-sqlite3'); -const path = require('path'); -const fs = require('fs'); -const { createDb } = require('../lib/indexer/db'); -const { reverseGeocode } = require('../lib/indexer/geocoder'); - -async function test() { - const dbPath = path.join(__dirname, 'test-memory.sqlite'); - if (fs.existsSync(dbPath)) fs.unlinkSync(dbPath); - - console.log('--- Testing Database Creation ---'); - const db = createDb(dbPath); - const tables = db.prepare("SELECT name FROM sqlite_master WHERE type='table'").all(); - console.log('Tables:', tables.map(t => t.name)); - - const hasCacheTable = tables.some(t => t.name === 'geocoding_cache'); - console.log('Has geocoding_cache:', hasCacheTable); - - if (!hasCacheTable) { - console.error('FAILED: geocoding_cache table missing'); - process.exit(1); - } - - console.log('\n--- Testing Geocoder Cache ---'); - const lat = 40.7128; - const lon = -74.0060; - - // First call (should hit network or fail gracefully if no internet, but here we check DB insert) - console.log('First call (simulated or real)...'); - const place = await reverseGeocode(db, lat, lon); - console.log('Result:', place); - - const cached = db.prepare('SELECT * FROM geocoding_cache').all(); - console.log('Cache contents:', cached); - - if (cached.length > 0 || place === null) { - console.log('SUCCESS: Geocoder handled request and DB cache interaction.'); - } else { - console.log('FAILED: No cache entry created.'); - } - - db.close(); - // fs.unlinkSync(dbPath); - process.exit(0); -} - -test().catch(console.error);