Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@ memory_graph.db
memory_graph.db-journal
.cache/
temp/
tmp/
logs/
*.log
*.sqlite

# Build & OS
dist/
Expand Down
12 changes: 0 additions & 12 deletions check_db.js

This file was deleted.

12 changes: 0 additions & 12 deletions check_db2.js

This file was deleted.

37 changes: 0 additions & 37 deletions debug-db.js

This file was deleted.

Binary file modified docs/images/timeline_2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 0 additions & 2 deletions lib/indexer.js
Original file line number Diff line number Diff line change
@@ -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,
};
5 changes: 2 additions & 3 deletions lib/indexer/repository.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
79 changes: 50 additions & 29 deletions lib/indexer/vector-search.js
Original file line number Diff line number Diff line change
@@ -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 [];
Expand Down
26 changes: 21 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
@@ -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 .",
Expand All @@ -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": {
Expand Down Expand Up @@ -41,17 +41,32 @@
"zip"
]
},
"mac": {
"target": [
"dmg",
"zip"
],
"category": "public.app-category.photography",
"darkModeSupport": true
},
"nsis": {
"oneClick": true,
"allowToChangeInstallationDirectory": false,
"createDesktopShortcut": true,
"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": [
"**/*",
Expand All @@ -60,7 +75,8 @@
"!docs",
"!.github",
"!*.log",
"!.gitignore"
"!.gitignore",
"!tmp"
]
}
}
1 change: 0 additions & 1 deletion preload.js

This file was deleted.

63 changes: 60 additions & 3 deletions src/preload/index.js
Original file line number Diff line number Diff line change
@@ -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'),
};
Expand Down
10 changes: 0 additions & 10 deletions src/renderer/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down
4 changes: 0 additions & 4 deletions src/renderer/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -462,10 +462,6 @@ button:active {
transform: translateY(0);
}

button:active {
transform: translateY(0);
}

#status {
position: fixed;
bottom: 2rem;
Expand Down
Loading
Loading