A local Python FastAPI application for storing and searching memories with images, text notes, richer memory metadata, and GPS coordinates using CLIP embeddings.
- Store Memories: Upload photos with optional notes, richer metadata, and GPS coordinates
- List & Retrieve Memories: Browse all memories or fetch a single one by id
- Serve Images: Dedicated endpoint for serving stored image files
- Semantic Search: Search memories using natural language queries with configurable result count
- CLIP Embeddings: Uses OpenAI's CLIP model for image and text embeddings (falls back to simple features when CLIP is unavailable)
- Cosine Similarity: Ranks matches by semantic similarity
- SQLite Storage: Local database with efficient BLOB storage for embeddings
- Image Storage: Saves uploaded images to local filesystem
Note:
memories.dband theimages/directory are local runtime data and are excluded from version control via.gitignore.
- Install Python dependencies:
pip install -r requirements.txt- Run the application:
python main.pyThe API will be available at http://127.0.0.1:8000
Store a new memory with a photo, optional text note/metadata, and GPS coordinates.
Parameters:
photo(file, required): Image file to uploadnote(string, optional): Text note describing the memoryuser_note(string, optional): What the user noticed/felt in their own wordsbepo_summary(string, optional): Optional concise assistant-style summary of the memorytags(string, optional): Comma-separated or free-form tagsmood(string, optional): Mood/emotion hint (for examplecalm,excited)place_hint(string, optional): Optional place cue (for examplenear red couch hallway)lat(float, optional): Latitude coordinatelon(float, optional): Longitude coordinate
Example:
curl -X POST "http://127.0.0.1:8000/memory" \
-F "photo=@path/to/image.jpg" \
-F "note=Beautiful sunset at the beach" \
-F "mood=calm" \
-F "tags=beach,sunset" \
-F "lat=34.0522" \
-F "lon=-118.2437"Response:
{
"status": "success",
"memory_id": 1,
"timestamp": "2024-01-01T12:00:00.000000",
"image_path": "images/20240101_120000_000000.jpg",
"image_url": "/image/1",
"note": "Beautiful sunset at the beach",
"user_note": null,
"bepo_summary": null,
"tags": "beach,sunset",
"mood": "calm",
"place_hint": null,
"lat": 34.0522,
"lon": -118.2437,
"map_url": "https://www.google.com/maps/search/?api=1&query=34.0522,-118.2437"
}Update editable memory metadata after a memory is created. This prepares Bepo for future chat-based memory annotation by letting later context refine what Bepo remembers about a photo.
Request body (JSON, all fields optional):
note(string ornull)user_note(string ornull)bepo_summary(string ornull)tags(string ornull)mood(string ornull)place_hint(string ornull)
If a field is omitted, the existing value is preserved. If a field is sent as null, that field is cleared. After every metadata update, Bepo rebuilds the text embedding from the current text fields so search reflects the updated memory meaning.
Example:
curl -X PATCH "http://127.0.0.1:8000/memory/1/metadata" \
-H "Content-Type: application/json" \
-d '{
"user_note": "I remember the quiet corner table",
"tags": "cafe,quiet,corner",
"mood": "peaceful",
"place_hint": "near the back window"
}'Response:
{
"id": 1,
"timestamp": "2024-01-01T12:00:00.000000",
"note": "Beautiful sunset at the beach",
"user_note": "I remember the quiet corner table",
"bepo_summary": null,
"tags": "cafe,quiet,corner",
"mood": "peaceful",
"place_hint": "near the back window",
"lat": 34.0522,
"lon": -118.2437,
"image_path": "images/20240101_120000_000000.jpg",
"image_url": "/image/1",
"map_url": "https://www.google.com/maps/search/?api=1&query=34.0522,-118.2437"
}Return all saved memories, newest first. Embeddings are not included.
Includes note, user_note, bepo_summary, tags, mood, place_hint, lat, lon, map_url, and image_url.
Example:
curl "http://127.0.0.1:8000/memories"Response:
[
{
"id": 1,
"timestamp": "2024-01-01T12:00:00.000000",
"note": "Beautiful sunset at the beach",
"lat": 34.0522,
"lon": -118.2437,
"image_path": "images/20240101_120000_000000.jpg",
"image_url": "/image/1",
"map_url": "https://www.google.com/maps/search/?api=1&query=34.0522,-118.2437"
}
]Return a single memory by id. Returns 404 if not found.
Includes note, user_note, bepo_summary, tags, mood, place_hint, lat, lon, map_url, and image_url.
Example:
curl "http://127.0.0.1:8000/memory/1"Response:
{
"id": 1,
"timestamp": "2024-01-01T12:00:00.000000",
"note": "Beautiful sunset at the beach",
"lat": 34.0522,
"lon": -118.2437,
"image_path": "images/20240101_120000_000000.jpg",
"image_url": "/image/1",
"map_url": "https://www.google.com/maps/search/?api=1&query=34.0522,-118.2437"
}Serve the image file associated with a memory. Designed for use by a mobile frontend. Returns 404 if the memory or its image file does not exist.
Example:
curl "http://127.0.0.1:8000/image/1" --output photo.jpgSearch memories using a text query. Returns the top top_k matches ranked by similarity score.
Parameters:
query(string, required): Search query text (must not be empty)top_k(int, optional, default 5, min 1, max 20): Number of results to return
Example:
curl -X POST "http://127.0.0.1:8000/search" \
-F "query=sunset" \
-F "top_k=3"Response (with results):
{
"status": "success",
"query": "sunset",
"count": 1,
"matches": [
{
"id": 1,
"timestamp": "2024-01-01T12:00:00.000000",
"image_path": "images/20240101_120000_000000.jpg",
"image_url": "/image/1",
"note": "Beautiful sunset at the beach",
"user_note": null,
"bepo_summary": null,
"tags": "beach,sunset",
"mood": "calm",
"place_hint": null,
"lat": 34.0522,
"lon": -118.2437,
"map_url": "https://www.google.com/maps/search/?api=1&query=34.0522,-118.2437",
"score": 0.87
}
]
}Response (empty database):
{
"status": "no_results",
"message": "No memories found in database",
"matches": []
}Ask a natural question about saved memories. Bepo retrieves the most relevant memories and returns a simple, friendly answer built from the top match's metadata. This is a local, deterministic chat-lite endpoint — it does not call any LLM or external API.
Request body (JSON):
message(string, required): Your question or description (must not be empty or whitespace-only)top_k(int, optional, default 3, min 1, max 10): Number of memories to retrieve
Example:
curl -X POST "http://127.0.0.1:8000/chat" \
-H "Content-Type: application/json" \
-d '{"message": "Where was that calm cafe with the cat?", "top_k": 3}'Response (with results):
{
"status": "success",
"message": "Where was that calm cafe with the cat?",
"answer": "You may mean the memory near the red couch hallway. I remember it as calm, cafe, cat, cozy.",
"count": 1,
"memories": [
{
"id": 1,
"timestamp": "2024-01-01T12:00:00.000000",
"note": null,
"user_note": null,
"bepo_summary": null,
"tags": "cafe,cat,cozy",
"mood": "calm",
"place_hint": "near the red couch hallway",
"lat": null,
"lon": null,
"image_path": "images/20240101_120000_000000.jpg",
"image_url": "/image/1",
"map_url": null,
"score": 0.72
}
]
}Response (empty database):
{
"status": "no_results",
"message": "Where was that calm cafe with the cat?",
"answer": "I do not have any memories saved yet.",
"memories": []
}The answer is built locally from the top memory's metadata (place hint, mood, tags, note/summary). No OpenAI or external API is involved.
Returns API version information and a list of available endpoints.
pip install -r requirements-dev.txt
python -m pytest tests/ -vTests disable CLIP/model downloads and run against temporary SQLite DB/filesystem paths (not your local memories.db or images/).
GitHub Actions runs python -m pytest tests/ -v on every pull request and on pushes to main.
CI sets TRANSFORMERS_OFFLINE=1 and HF_HUB_OFFLINE=1 defensively so tests never rely on model downloads.
CREATE TABLE memories (
id INTEGER PRIMARY KEY AUTOINCREMENT,
ts DATETIME NOT NULL,
lat REAL,
lon REAL,
image_path TEXT NOT NULL,
image_emb BLOB NOT NULL,
text_note TEXT,
text_emb BLOB,
user_note TEXT,
bepo_summary TEXT,
tags TEXT,
mood TEXT,
place_hint TEXT
);text_note is kept for backward compatibility. On startup, Bepo runs a migration-safe schema check (PRAGMA table_info(memories) + ALTER TABLE ... ADD COLUMN) so existing memories.db files are upgraded in place without dropping data.
For semantic search, Bepo builds text embeddings from all available memory text fields (note, user_note, bepo_summary, tags, mood, place_hint). These richer fields are intended for future Bepo chat and personal recall features.
- FastAPI: Web framework for the REST API
- SQLite: Local database for storing memories and embeddings
- CLIP (openai/clip-vit-base-patch32): Generates embeddings for images and text
- PIL (Pillow): Image processing
- PyTorch: Deep learning framework for CLIP
- NumPy: Efficient array operations and cosine similarity calculation
This application runs entirely locally:
- No external API calls (except for initial CLIP model download)
- All data stored locally in SQLite database (
memories.db) - Images saved to local
images/directory - Both
memories.dbandimages/are excluded from git - All processing done on your machine