Ubuntu commited on
Commit ·
8c9a0f8
1
Parent(s): 9af0669
workign 2 dbs need toe be in server
Browse files- .dockerignore +73 -0
- .gitignore +1 -0
- Dockerfile +8 -0
- README.md +120 -10
- app.py +226 -39
- database.py +296 -0
- main.py +224 -14
- pyproject.toml +2 -0
- seed_data.json +8 -0
- seed_db.py +109 -0
- semantic_search.py +147 -0
- uv.lock +0 -0
.dockerignore
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Python
|
| 2 |
+
__pycache__/
|
| 3 |
+
*.py[cod]
|
| 4 |
+
*$py.class
|
| 5 |
+
*.so
|
| 6 |
+
.Python
|
| 7 |
+
*.egg-info/
|
| 8 |
+
*.egg
|
| 9 |
+
|
| 10 |
+
# Virtual environments (BIG!)
|
| 11 |
+
.venv/
|
| 12 |
+
venv/
|
| 13 |
+
ENV/
|
| 14 |
+
env/
|
| 15 |
+
|
| 16 |
+
# Database files (seeded at build time for consistency)
|
| 17 |
+
music_memories.db
|
| 18 |
+
*.db
|
| 19 |
+
*.sqlite
|
| 20 |
+
*.sqlite3
|
| 21 |
+
|
| 22 |
+
# ChromaDB (regenerate on build)
|
| 23 |
+
chroma_db/
|
| 24 |
+
|
| 25 |
+
# IDE
|
| 26 |
+
.idea/
|
| 27 |
+
.vscode/
|
| 28 |
+
*.swp
|
| 29 |
+
*.swo
|
| 30 |
+
|
| 31 |
+
# Testing
|
| 32 |
+
.pytest_cache/
|
| 33 |
+
.coverage
|
| 34 |
+
htmlcov/
|
| 35 |
+
|
| 36 |
+
# Jupyter
|
| 37 |
+
.ipynb_checkpoints
|
| 38 |
+
|
| 39 |
+
# ML models (too big)
|
| 40 |
+
*.pth
|
| 41 |
+
*.pt
|
| 42 |
+
*.onnx
|
| 43 |
+
*.pb
|
| 44 |
+
*.h5
|
| 45 |
+
*.pkl
|
| 46 |
+
*.pickle
|
| 47 |
+
*.ckpt
|
| 48 |
+
*.safetensors
|
| 49 |
+
saved_model/
|
| 50 |
+
|
| 51 |
+
# Logs
|
| 52 |
+
*.log
|
| 53 |
+
logs/
|
| 54 |
+
|
| 55 |
+
# OS
|
| 56 |
+
.DS_Store
|
| 57 |
+
Thumbs.db
|
| 58 |
+
|
| 59 |
+
# Environment
|
| 60 |
+
.env
|
| 61 |
+
.env.local
|
| 62 |
+
|
| 63 |
+
# Git
|
| 64 |
+
.git/
|
| 65 |
+
.gitignore
|
| 66 |
+
|
| 67 |
+
# Docker
|
| 68 |
+
Dockerfile
|
| 69 |
+
docker-compose*.yml
|
| 70 |
+
|
| 71 |
+
# Documentation (not needed in image)
|
| 72 |
+
*.md
|
| 73 |
+
!README.md
|
.gitignore
CHANGED
|
@@ -85,3 +85,4 @@ uv.lock
|
|
| 85 |
# Local development
|
| 86 |
*.local
|
| 87 |
*.dev
|
|
|
|
|
|
| 85 |
# Local development
|
| 86 |
*.local
|
| 87 |
*.dev
|
| 88 |
+
|
Dockerfile
CHANGED
|
@@ -9,6 +9,8 @@ USER user
|
|
| 9 |
|
| 10 |
# uv installs binaries into ~/.local/bin
|
| 11 |
ENV PATH="/home/user/.local/bin:/app/.venv/bin:$PATH"
|
|
|
|
|
|
|
| 12 |
WORKDIR /app
|
| 13 |
|
| 14 |
# Install uv (dependency manager)
|
|
@@ -24,6 +26,12 @@ RUN uv venv /app/.venv \
|
|
| 24 |
# Copy the rest of the app
|
| 25 |
COPY --chown=user . /app
|
| 26 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 27 |
EXPOSE 7860
|
| 28 |
|
| 29 |
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "7860"]
|
|
|
|
| 9 |
|
| 10 |
# uv installs binaries into ~/.local/bin
|
| 11 |
ENV PATH="/home/user/.local/bin:/app/.venv/bin:$PATH"
|
| 12 |
+
ENV CHROMA_DB_HOST_IP="127.0.0.1"
|
| 13 |
+
ENV ANONYMIZED_TELEMETRY="false"
|
| 14 |
WORKDIR /app
|
| 15 |
|
| 16 |
# Install uv (dependency manager)
|
|
|
|
| 26 |
# Copy the rest of the app
|
| 27 |
COPY --chown=user . /app
|
| 28 |
|
| 29 |
+
# Ensure data directories exist and are writable
|
| 30 |
+
RUN mkdir -p /app/chroma_db /app/data && chown -R user:user /app/chroma_db /app/data
|
| 31 |
+
|
| 32 |
+
# Seed the database (consistent data in every container)
|
| 33 |
+
RUN uv run python seed_db.py
|
| 34 |
+
|
| 35 |
EXPOSE 7860
|
| 36 |
|
| 37 |
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "7860"]
|
README.md
CHANGED
|
@@ -1,10 +1,120 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
--
|
| 9 |
-
|
| 10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 🎵 Music Memories
|
| 2 |
+
|
| 3 |
+
A music library and memory management app with semantic search powered by ChromaDB and SQLite.
|
| 4 |
+
|
| 5 |
+
## Features
|
| 6 |
+
|
| 7 |
+
- **Songs** - Store songs with metadata (title, artist, album, BPM, energy level) and lyrics
|
| 8 |
+
- **Users** - Manage user accounts
|
| 9 |
+
- **Memories** - Link memories to songs and users
|
| 10 |
+
- **Playlists** - Create playlists with mood descriptions
|
| 11 |
+
- **Contexts** - Track listening contexts (weather, time, location)
|
| 12 |
+
- **Play History** - Log listening history
|
| 13 |
+
- **Semantic Search** - Search songs, memories, and playlists by vibe/mood using AI embeddings
|
| 14 |
+
|
| 15 |
+
## Tech Stack
|
| 16 |
+
|
| 17 |
+
- **FastAPI** - REST API
|
| 18 |
+
- **SQLite** - Permanent storage for all data
|
| 19 |
+
- **ChromaDB** - Semantic search embeddings
|
| 20 |
+
- **Sentence Transformers** - all-MiniLM-L6-v2 for embeddings
|
| 21 |
+
- **Gradio** - Web UI
|
| 22 |
+
|
| 23 |
+
## API Endpoints
|
| 24 |
+
|
| 25 |
+
### Songs
|
| 26 |
+
| Method | Endpoint | Description |
|
| 27 |
+
|--------|----------|-------------|
|
| 28 |
+
| GET | `/songs` | List all songs |
|
| 29 |
+
| GET | `/songs/{id}` | Get song by ID |
|
| 30 |
+
| POST | `/songs?title=X&artist=Y&lyrics=Z` | Add song |
|
| 31 |
+
| DELETE | `/songs/{id}` | Delete song |
|
| 32 |
+
|
| 33 |
+
### Users
|
| 34 |
+
| Method | Endpoint | Description |
|
| 35 |
+
|--------|----------|-------------|
|
| 36 |
+
| GET | `/users` | List all users |
|
| 37 |
+
| GET | `/users/{id}` | Get user by ID |
|
| 38 |
+
| POST | `/users?name=X` | Add user |
|
| 39 |
+
|
| 40 |
+
### Memories
|
| 41 |
+
| Method | Endpoint | Description |
|
| 42 |
+
|--------|----------|-------------|
|
| 43 |
+
| GET | `/memories` | List all memories |
|
| 44 |
+
| GET | `/users/{id}/memories` | Get user's memories |
|
| 45 |
+
| POST | `/memories?user_id=X&description=Y` | Add memory |
|
| 46 |
+
| DELETE | `/memories/{id}` | Delete memory |
|
| 47 |
+
|
| 48 |
+
### Playlists
|
| 49 |
+
| Method | Endpoint | Description |
|
| 50 |
+
|--------|----------|-------------|
|
| 51 |
+
| GET | `/playlists` | List all playlists |
|
| 52 |
+
| POST | `/playlists?name=X&mood_description=Y` | Add playlist |
|
| 53 |
+
| DELETE | `/playlists/{id}` | Delete playlist |
|
| 54 |
+
|
| 55 |
+
### Contexts
|
| 56 |
+
| Method | Endpoint | Description |
|
| 57 |
+
|--------|----------|-------------|
|
| 58 |
+
| GET | `/contexts` | List all contexts |
|
| 59 |
+
| POST | `/contexts?weather=X&time_of_day=Y` | Add context |
|
| 60 |
+
| DELETE | `/contexts/{id}` | Delete context |
|
| 61 |
+
|
| 62 |
+
### Play History
|
| 63 |
+
| Method | Endpoint | Description |
|
| 64 |
+
|--------|----------|-------------|
|
| 65 |
+
| GET | `/history?user_id=X` | Get play history |
|
| 66 |
+
| POST | `/history?user_id=X&song_id=Y` | Add play entry |
|
| 67 |
+
|
| 68 |
+
### Semantic Search
|
| 69 |
+
| Method | Endpoint | Description |
|
| 70 |
+
|--------|----------|-------------|
|
| 71 |
+
| GET | `/search/songs?q=query&n=5` | Search songs by vibe |
|
| 72 |
+
| GET | `/search/memories?q=query&n=5` | Search memories |
|
| 73 |
+
| GET | `/search/playlists?q=query&n=5` | Search playlists by mood |
|
| 74 |
+
| GET | `/search/contexts?q=query&n=5` | Search contexts |
|
| 75 |
+
|
| 76 |
+
## Example Usage
|
| 77 |
+
|
| 78 |
+
```bash
|
| 79 |
+
# Add a user
|
| 80 |
+
curl -X POST "http://localhost:7860/users?name=Alice"
|
| 81 |
+
|
| 82 |
+
# Add a song with lyrics for semantic search
|
| 83 |
+
curl -X POST "http://localhost:7860/songs?title=Midnight%20Rain&artist=Taylor%20Swift&energy_level=6&lyrics=He%20wanted%20it%20comfortable%20quiet%20life"
|
| 84 |
+
|
| 85 |
+
# Add a memory
|
| 86 |
+
curl -X POST "http://localhost:7860/memories?user_id=1&description=Dancing in the rain on our first date&song_id=1"
|
| 87 |
+
|
| 88 |
+
# Search songs by vibe
|
| 89 |
+
curl "http://localhost:7860/search/songs?q=sad%20breakup&n=5"
|
| 90 |
+
|
| 91 |
+
# Search memories
|
| 92 |
+
curl "http://localhost:7860/search/memories?q=romantic%20evening&n=5"
|
| 93 |
+
|
| 94 |
+
# Search playlists by mood
|
| 95 |
+
curl "http://localhost:7860/search/playlists?q=chill%20evening%20vibes&n=5"
|
| 96 |
+
```
|
| 97 |
+
|
| 98 |
+
## Run Locally
|
| 99 |
+
|
| 100 |
+
```bash
|
| 101 |
+
# Install dependencies
|
| 102 |
+
uv sync
|
| 103 |
+
|
| 104 |
+
# Run the server
|
| 105 |
+
uvicorn main:app --host 0.0.0.0 --port 7860
|
| 106 |
+
```
|
| 107 |
+
|
| 108 |
+
Visit `http://localhost:7860/ui` for the web interface.
|
| 109 |
+
|
| 110 |
+
## Docker
|
| 111 |
+
|
| 112 |
+
```bash
|
| 113 |
+
docker build -t music-memories .
|
| 114 |
+
docker run -p 7860:7860 -v ./data:/app/data music-memories
|
| 115 |
+
```
|
| 116 |
+
|
| 117 |
+
## Data Storage
|
| 118 |
+
|
| 119 |
+
- **SQLite**: `music_memories.db` - All structured data
|
| 120 |
+
- **ChromaDB**: `chroma_db/` - Semantic search embeddings
|
app.py
CHANGED
|
@@ -5,63 +5,250 @@ BASE_URL = "http://0.0.0.0:7860"
|
|
| 5 |
|
| 6 |
|
| 7 |
def create_gradio_app():
|
| 8 |
-
"""Create and return the Gradio Blocks app
|
| 9 |
-
|
| 10 |
-
def
|
| 11 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
try:
|
| 13 |
with httpx.Client() as client:
|
| 14 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
resp.raise_for_status()
|
| 16 |
data = resp.json()
|
| 17 |
-
|
|
|
|
|
|
|
| 18 |
except Exception as e:
|
| 19 |
-
return
|
| 20 |
|
| 21 |
-
def
|
| 22 |
-
"""Call the products endpoint."""
|
| 23 |
try:
|
| 24 |
with httpx.Client() as client:
|
| 25 |
-
resp = client.get(f"{BASE_URL}/
|
| 26 |
resp.raise_for_status()
|
| 27 |
data = resp.json()
|
| 28 |
-
|
|
|
|
|
|
|
| 29 |
except Exception as e:
|
| 30 |
-
return
|
| 31 |
|
| 32 |
-
def
|
| 33 |
-
"""Call the health endpoint."""
|
| 34 |
try:
|
| 35 |
with httpx.Client() as client:
|
| 36 |
-
resp = client.get(f"{BASE_URL}/
|
| 37 |
resp.raise_for_status()
|
| 38 |
-
|
|
|
|
|
|
|
|
|
|
| 39 |
except Exception as e:
|
| 40 |
-
return
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 62 |
health_output = gr.JSON(label="Health Status")
|
| 63 |
health_btn = gr.Button("Check Health")
|
| 64 |
-
health_btn.click(fn=
|
| 65 |
|
| 66 |
return demo
|
| 67 |
|
|
|
|
| 5 |
|
| 6 |
|
| 7 |
def create_gradio_app():
|
| 8 |
+
"""Create and return the Gradio Blocks app for Music Memories."""
|
| 9 |
+
|
| 10 |
+
def add_song_fn(title, artist, album, duration, bpm, energy_level, lyrics):
|
| 11 |
+
try:
|
| 12 |
+
with httpx.Client() as client:
|
| 13 |
+
params = {"title": title, "artist": artist}
|
| 14 |
+
if album: params["album"] = album
|
| 15 |
+
if duration: params["duration"] = int(duration)
|
| 16 |
+
if bpm: params["bpm"] = int(bpm)
|
| 17 |
+
if energy_level: params["energy_level"] = int(energy_level)
|
| 18 |
+
if lyrics: params["lyrics"] = lyrics
|
| 19 |
+
resp = client.post(f"{BASE_URL}/songs", params=params, timeout=10.0)
|
| 20 |
+
resp.raise_for_status()
|
| 21 |
+
return "Success!", str(resp.json())
|
| 22 |
+
except Exception as e:
|
| 23 |
+
return "Error", str(e)
|
| 24 |
+
|
| 25 |
+
def add_user_fn(name):
|
| 26 |
+
try:
|
| 27 |
+
with httpx.Client() as client:
|
| 28 |
+
resp = client.post(f"{BASE_URL}/users", params={"name": name}, timeout=5.0)
|
| 29 |
+
resp.raise_for_status()
|
| 30 |
+
return "Success!", str(resp.json())
|
| 31 |
+
except Exception as e:
|
| 32 |
+
return "Error", str(e)
|
| 33 |
+
|
| 34 |
+
def add_memory_fn(user_id, description, date, song_id):
|
| 35 |
+
try:
|
| 36 |
+
with httpx.Client() as client:
|
| 37 |
+
params = {"user_id": int(user_id), "description": description}
|
| 38 |
+
if date: params["date"] = date
|
| 39 |
+
if song_id: params["song_id"] = int(song_id)
|
| 40 |
+
resp = client.post(f"{BASE_URL}/memories", params=params, timeout=10.0)
|
| 41 |
+
resp.raise_for_status()
|
| 42 |
+
return "Success!", str(resp.json())
|
| 43 |
+
except Exception as e:
|
| 44 |
+
return "Error", str(e)
|
| 45 |
+
|
| 46 |
+
def add_playlist_fn(name, vibe_code, mood_description):
|
| 47 |
+
try:
|
| 48 |
+
with httpx.Client() as client:
|
| 49 |
+
params = {"name": name}
|
| 50 |
+
if vibe_code: params["vibe_code"] = vibe_code
|
| 51 |
+
if mood_description: params["mood_description"] = mood_description
|
| 52 |
+
resp = client.post(f"{BASE_URL}/playlists", params=params, timeout=10.0)
|
| 53 |
+
resp.raise_for_status()
|
| 54 |
+
return "Success!", str(resp.json())
|
| 55 |
+
except Exception as e:
|
| 56 |
+
return "Error", str(e)
|
| 57 |
+
|
| 58 |
+
def add_context_fn(weather, time_of_day, location_type):
|
| 59 |
try:
|
| 60 |
with httpx.Client() as client:
|
| 61 |
+
params = {}
|
| 62 |
+
if weather: params["weather"] = weather
|
| 63 |
+
if time_of_day: params["time_of_day"] = time_of_day
|
| 64 |
+
if location_type: params["location_type"] = location_type
|
| 65 |
+
resp = client.post(f"{BASE_URL}/contexts", params=params, timeout=5.0)
|
| 66 |
+
resp.raise_for_status()
|
| 67 |
+
return "Success!", str(resp.json())
|
| 68 |
+
except Exception as e:
|
| 69 |
+
return "Error", str(e)
|
| 70 |
+
|
| 71 |
+
def search_songs_fn(query, n_results):
|
| 72 |
+
try:
|
| 73 |
+
with httpx.Client() as client:
|
| 74 |
+
resp = client.get(f"{BASE_URL}/search/songs", params={"q": query, "n": n_results}, timeout=10.0)
|
| 75 |
resp.raise_for_status()
|
| 76 |
data = resp.json()
|
| 77 |
+
results = data.get("results", [])
|
| 78 |
+
table = [[r["id"], r["title"], r["artist"], f"{r['distance']:.4f}"] for r in results] if results else []
|
| 79 |
+
return f"Found {len(results)} songs", table
|
| 80 |
except Exception as e:
|
| 81 |
+
return "Error", []
|
| 82 |
|
| 83 |
+
def search_memories_fn(query, n_results):
|
|
|
|
| 84 |
try:
|
| 85 |
with httpx.Client() as client:
|
| 86 |
+
resp = client.get(f"{BASE_URL}/search/memories", params={"q": query, "n": n_results}, timeout=10.0)
|
| 87 |
resp.raise_for_status()
|
| 88 |
data = resp.json()
|
| 89 |
+
results = data.get("results", [])
|
| 90 |
+
table = [[r["id"], r["user_id"], r["document"][:50], f"{r['distance']:.4f}"] for r in results] if results else []
|
| 91 |
+
return f"Found {len(results)} memories", table
|
| 92 |
except Exception as e:
|
| 93 |
+
return "Error", []
|
| 94 |
|
| 95 |
+
def search_playlists_fn(query, n_results):
|
|
|
|
| 96 |
try:
|
| 97 |
with httpx.Client() as client:
|
| 98 |
+
resp = client.get(f"{BASE_URL}/search/playlists", params={"q": query, "n": n_results}, timeout=10.0)
|
| 99 |
resp.raise_for_status()
|
| 100 |
+
data = resp.json()
|
| 101 |
+
results = data.get("results", [])
|
| 102 |
+
table = [[r["id"], r["name"], f"{r['distance']:.4f}"] for r in results] if results else []
|
| 103 |
+
return f"Found {len(results)} playlists", table
|
| 104 |
except Exception as e:
|
| 105 |
+
return "Error", []
|
| 106 |
+
|
| 107 |
+
def list_songs_fn():
|
| 108 |
+
try:
|
| 109 |
+
with httpx.Client() as client:
|
| 110 |
+
resp = client.get(f"{BASE_URL}/songs", timeout=5.0)
|
| 111 |
+
resp.raise_for_status()
|
| 112 |
+
data = resp.json()
|
| 113 |
+
songs = data.get("songs", [])
|
| 114 |
+
return [[s["id"], s["title"], s["artist"], s.get("album",""), s.get("bpm","")] for s in songs]
|
| 115 |
+
except Exception as e:
|
| 116 |
+
return [["Error", str(e), "", "", ""]]
|
| 117 |
+
|
| 118 |
+
def list_users_fn():
|
| 119 |
+
try:
|
| 120 |
+
with httpx.Client() as client:
|
| 121 |
+
resp = client.get(f"{BASE_URL}/users", timeout=5.0)
|
| 122 |
+
resp.raise_for_status()
|
| 123 |
+
data = resp.json()
|
| 124 |
+
users = data.get("users", [])
|
| 125 |
+
return [[u["id"], u["name"], u.get("created_at","")] for u in users]
|
| 126 |
+
except Exception as e:
|
| 127 |
+
return [["Error", str(e), ""]]
|
| 128 |
+
|
| 129 |
+
def list_memories_fn():
|
| 130 |
+
try:
|
| 131 |
+
with httpx.Client() as client:
|
| 132 |
+
resp = client.get(f"{BASE_URL}/memories", timeout=5.0)
|
| 133 |
+
resp.raise_for_status()
|
| 134 |
+
data = resp.json()
|
| 135 |
+
memories = data.get("memories", [])
|
| 136 |
+
return [[m["id"], m["user_id"], m["description"][:50], m.get("date","")] for m in memories]
|
| 137 |
+
except Exception as e:
|
| 138 |
+
return [["Error", str(e), "", ""]]
|
| 139 |
+
|
| 140 |
+
with gr.Blocks(title="Music Memories UI") as demo:
|
| 141 |
+
gr.Markdown("# 🎵 Music Memories")
|
| 142 |
+
gr.Markdown("Manage your music library, memories, and playlists with semantic search.")
|
| 143 |
+
|
| 144 |
+
with gr.Tab("🎶 Songs"):
|
| 145 |
+
with gr.Group():
|
| 146 |
+
gr.Markdown("### Add Song")
|
| 147 |
+
song_title = gr.Textbox(label="Title", placeholder="Song title")
|
| 148 |
+
song_artist = gr.Textbox(label="Artist", placeholder="Artist name")
|
| 149 |
+
song_album = gr.Textbox(label="Album", placeholder="Album (optional)")
|
| 150 |
+
song_duration = gr.Number(label="Duration (seconds)", precision=0)
|
| 151 |
+
song_bpm = gr.Number(label="BPM", precision=0)
|
| 152 |
+
song_energy = gr.Slider(1, 10, value=5, label="Energy Level")
|
| 153 |
+
song_lyrics = gr.Textbox(label="Lyrics", placeholder="Lyrics for semantic search", lines=3)
|
| 154 |
+
song_add_btn = gr.Button("Add Song")
|
| 155 |
+
song_add_status = gr.Textbox(label="Status")
|
| 156 |
+
song_add_output = gr.JSON(label="Response")
|
| 157 |
+
song_add_btn.click(fn=add_song_fn, inputs=[song_title, song_artist, song_album, song_duration, song_bpm, song_energy, song_lyrics], outputs=[song_add_status, song_add_output])
|
| 158 |
+
|
| 159 |
+
with gr.Group():
|
| 160 |
+
gr.Markdown("### All Songs")
|
| 161 |
+
songs_table = gr.Dataframe(headers=["ID", "Title", "Artist", "Album", "BPM"], label="Songs")
|
| 162 |
+
songs_load_btn = gr.Button("Load Songs")
|
| 163 |
+
songs_load_btn.click(fn=list_songs_fn, outputs=songs_table)
|
| 164 |
+
|
| 165 |
+
with gr.Tab("👤 Users"):
|
| 166 |
+
with gr.Group():
|
| 167 |
+
gr.Markdown("### Add User")
|
| 168 |
+
user_name = gr.Textbox(label="Name", placeholder="User name")
|
| 169 |
+
user_add_btn = gr.Button("Add User")
|
| 170 |
+
user_add_status = gr.Textbox(label="Status")
|
| 171 |
+
user_add_output = gr.JSON(label="Response")
|
| 172 |
+
user_add_btn.click(fn=add_user_fn, inputs=user_name, outputs=[user_add_status, user_add_output])
|
| 173 |
+
|
| 174 |
+
with gr.Group():
|
| 175 |
+
gr.Markdown("### All Users")
|
| 176 |
+
users_table = gr.Dataframe(headers=["ID", "Name", "Created At"], label="Users")
|
| 177 |
+
users_load_btn = gr.Button("Load Users")
|
| 178 |
+
users_load_btn.click(fn=list_users_fn, outputs=users_table)
|
| 179 |
+
|
| 180 |
+
with gr.Tab("💭 Memories"):
|
| 181 |
+
with gr.Group():
|
| 182 |
+
gr.Markdown("### Add Memory")
|
| 183 |
+
mem_user_id = gr.Number(label="User ID", precision=0)
|
| 184 |
+
mem_desc = gr.Textbox(label="Description", placeholder="Describe the memory...", lines=2)
|
| 185 |
+
mem_date = gr.Textbox(label="Date (optional)", placeholder="YYYY-MM-DD")
|
| 186 |
+
mem_song_id = gr.Number(label="Song ID (optional)", precision=0)
|
| 187 |
+
mem_add_btn = gr.Button("Add Memory")
|
| 188 |
+
mem_add_status = gr.Textbox(label="Status")
|
| 189 |
+
mem_add_output = gr.JSON(label="Response")
|
| 190 |
+
mem_add_btn.click(fn=add_memory_fn, inputs=[mem_user_id, mem_desc, mem_date, mem_song_id], outputs=[mem_add_status, mem_add_output])
|
| 191 |
+
|
| 192 |
+
with gr.Group():
|
| 193 |
+
gr.Markdown("### All Memories")
|
| 194 |
+
memories_table = gr.Dataframe(headers=["ID", "User ID", "Description", "Date"], label="Memories")
|
| 195 |
+
memories_load_btn = gr.Button("Load Memories")
|
| 196 |
+
memories_load_btn.click(fn=list_memories_fn, outputs=memories_table)
|
| 197 |
+
|
| 198 |
+
with gr.Tab("🎵 Playlists"):
|
| 199 |
+
with gr.Group():
|
| 200 |
+
gr.Markdown("### Add Playlist")
|
| 201 |
+
pl_name = gr.Textbox(label="Name", placeholder="Playlist name")
|
| 202 |
+
pl_vibe = gr.Textbox(label="Vibe Code", placeholder="e.g., chill, energetic")
|
| 203 |
+
pl_mood = gr.Textbox(label="Mood Description", placeholder="Describe the mood journey...", lines=2)
|
| 204 |
+
pl_add_btn = gr.Button("Add Playlist")
|
| 205 |
+
pl_add_status = gr.Textbox(label="Status")
|
| 206 |
+
pl_add_output = gr.JSON(label="Response")
|
| 207 |
+
pl_add_btn.click(fn=add_playlist_fn, inputs=[pl_name, pl_vibe, pl_mood], outputs=[pl_add_status, pl_add_output])
|
| 208 |
+
|
| 209 |
+
with gr.Tab("🌤️ Contexts"):
|
| 210 |
+
with gr.Group():
|
| 211 |
+
gr.Markdown("### Add Context")
|
| 212 |
+
ctx_weather = gr.Textbox(label="Weather", placeholder="e.g., rainy, sunny")
|
| 213 |
+
ctx_time = gr.Textbox(label="Time of Day", placeholder="e.g., morning, night")
|
| 214 |
+
ctx_location = gr.Textbox(label="Location Type", placeholder="e.g., home, gym, car")
|
| 215 |
+
ctx_add_btn = gr.Button("Add Context")
|
| 216 |
+
ctx_add_status = gr.Textbox(label="Status")
|
| 217 |
+
ctx_add_output = gr.JSON(label="Response")
|
| 218 |
+
ctx_add_btn.click(fn=add_context_fn, inputs=[ctx_weather, ctx_time, ctx_location], outputs=[ctx_add_status, ctx_add_output])
|
| 219 |
+
|
| 220 |
+
with gr.Tab("🔍 Semantic Search"):
|
| 221 |
+
with gr.Group():
|
| 222 |
+
gr.Markdown("### Search Songs by Vibe")
|
| 223 |
+
search_songs_query = gr.Textbox(label="Query", placeholder="e.g., 'sad breakup song', 'workout energy'")
|
| 224 |
+
search_songs_n = gr.Slider(1, 20, value=5, step=1, label="Results")
|
| 225 |
+
search_songs_btn = gr.Button("Search")
|
| 226 |
+
search_songs_summary = gr.Textbox(label="Results")
|
| 227 |
+
search_songs_table = gr.Dataframe(headers=["ID", "Title", "Artist", "Distance"], label="Songs")
|
| 228 |
+
search_songs_btn.click(fn=search_songs_fn, inputs=[search_songs_query, search_songs_n], outputs=[search_songs_summary, search_songs_table])
|
| 229 |
+
|
| 230 |
+
with gr.Group():
|
| 231 |
+
gr.Markdown("### Search Memories")
|
| 232 |
+
search_mem_query = gr.Textbox(label="Query", placeholder="e.g., 'summer road trip', 'first dance'")
|
| 233 |
+
search_mem_n = gr.Slider(1, 20, value=5, step=1, label="Results")
|
| 234 |
+
search_mem_btn = gr.Button("Search")
|
| 235 |
+
search_mem_summary = gr.Textbox(label="Results")
|
| 236 |
+
search_mem_table = gr.Dataframe(headers=["ID", "User ID", "Description", "Distance"], label="Memories")
|
| 237 |
+
search_mem_btn.click(fn=search_memories_fn, inputs=[search_mem_query, search_mem_n], outputs=[search_mem_summary, search_mem_table])
|
| 238 |
+
|
| 239 |
+
with gr.Group():
|
| 240 |
+
gr.Markdown("### Search Playlists by Mood")
|
| 241 |
+
search_pl_query = gr.Textbox(label="Query", placeholder="e.g., 'chill evening vibes', 'party energy'")
|
| 242 |
+
search_pl_n = gr.Slider(1, 20, value=5, step=1, label="Results")
|
| 243 |
+
search_pl_btn = gr.Button("Search")
|
| 244 |
+
search_pl_summary = gr.Textbox(label="Results")
|
| 245 |
+
search_pl_table = gr.Dataframe(headers=["ID", "Name", "Distance"], label="Playlists")
|
| 246 |
+
search_pl_btn.click(fn=search_playlists_fn, inputs=[search_pl_query, search_pl_n], outputs=[search_pl_summary, search_pl_table])
|
| 247 |
+
|
| 248 |
+
with gr.Tab("ℹ️ Health"):
|
| 249 |
health_output = gr.JSON(label="Health Status")
|
| 250 |
health_btn = gr.Button("Check Health")
|
| 251 |
+
health_btn.click(fn=lambda: {"status": "healthy", "app": "Music Memories"}, outputs=health_output)
|
| 252 |
|
| 253 |
return demo
|
| 254 |
|
database.py
ADDED
|
@@ -0,0 +1,296 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""SQLite database module for music memories app."""
|
| 2 |
+
|
| 3 |
+
import sqlite3
|
| 4 |
+
from contextlib import contextmanager
|
| 5 |
+
from datetime import datetime
|
| 6 |
+
from typing import Optional
|
| 7 |
+
|
| 8 |
+
DATABASE_PATH = "./music_memories.db"
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
@contextmanager
|
| 12 |
+
def get_db_connection():
|
| 13 |
+
"""Get a database connection with row factory."""
|
| 14 |
+
conn = sqlite3.connect(DATABASE_PATH)
|
| 15 |
+
conn.row_factory = sqlite3.Row
|
| 16 |
+
try:
|
| 17 |
+
yield conn
|
| 18 |
+
finally:
|
| 19 |
+
conn.close()
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
def init_database() -> None:
|
| 23 |
+
"""Initialize the SQLite database with all tables."""
|
| 24 |
+
with get_db_connection() as conn:
|
| 25 |
+
# Songs table
|
| 26 |
+
conn.execute("""
|
| 27 |
+
CREATE TABLE IF NOT EXISTS songs (
|
| 28 |
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 29 |
+
title TEXT NOT NULL,
|
| 30 |
+
artist TEXT NOT NULL,
|
| 31 |
+
album TEXT,
|
| 32 |
+
duration INTEGER,
|
| 33 |
+
bpm INTEGER,
|
| 34 |
+
energy_level INTEGER CHECK(energy_level BETWEEN 1 AND 10)
|
| 35 |
+
)
|
| 36 |
+
""")
|
| 37 |
+
|
| 38 |
+
# Users table
|
| 39 |
+
conn.execute("""
|
| 40 |
+
CREATE TABLE IF NOT EXISTS users (
|
| 41 |
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 42 |
+
name TEXT NOT NULL,
|
| 43 |
+
created_at TEXT DEFAULT CURRENT_TIMESTAMP
|
| 44 |
+
)
|
| 45 |
+
""")
|
| 46 |
+
|
| 47 |
+
# Playlists table
|
| 48 |
+
conn.execute("""
|
| 49 |
+
CREATE TABLE IF NOT EXISTS playlists (
|
| 50 |
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 51 |
+
name TEXT NOT NULL,
|
| 52 |
+
vibe_code TEXT,
|
| 53 |
+
created_at TEXT DEFAULT CURRENT_TIMESTAMP
|
| 54 |
+
)
|
| 55 |
+
""")
|
| 56 |
+
|
| 57 |
+
# Contexts table
|
| 58 |
+
conn.execute("""
|
| 59 |
+
CREATE TABLE IF NOT EXISTS contexts (
|
| 60 |
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 61 |
+
weather TEXT,
|
| 62 |
+
time_of_day TEXT,
|
| 63 |
+
location_type TEXT
|
| 64 |
+
)
|
| 65 |
+
""")
|
| 66 |
+
|
| 67 |
+
# Memories table
|
| 68 |
+
conn.execute("""
|
| 69 |
+
CREATE TABLE IF NOT EXISTS memories (
|
| 70 |
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 71 |
+
user_id INTEGER NOT NULL,
|
| 72 |
+
description TEXT NOT NULL,
|
| 73 |
+
date TEXT,
|
| 74 |
+
song_id INTEGER,
|
| 75 |
+
FOREIGN KEY (user_id) REFERENCES users(id),
|
| 76 |
+
FOREIGN KEY (song_id) REFERENCES songs(id)
|
| 77 |
+
)
|
| 78 |
+
""")
|
| 79 |
+
|
| 80 |
+
# Play history table (with composite primary key)
|
| 81 |
+
conn.execute("""
|
| 82 |
+
CREATE TABLE IF NOT EXISTS play_history (
|
| 83 |
+
user_id INTEGER NOT NULL,
|
| 84 |
+
song_id INTEGER NOT NULL,
|
| 85 |
+
played_at TEXT DEFAULT CURRENT_TIMESTAMP,
|
| 86 |
+
context_id INTEGER,
|
| 87 |
+
PRIMARY KEY (user_id, song_id, played_at),
|
| 88 |
+
FOREIGN KEY (user_id) REFERENCES users(id),
|
| 89 |
+
FOREIGN KEY (song_id) REFERENCES songs(id),
|
| 90 |
+
FOREIGN KEY (context_id) REFERENCES contexts(id)
|
| 91 |
+
)
|
| 92 |
+
""")
|
| 93 |
+
|
| 94 |
+
conn.commit()
|
| 95 |
+
|
| 96 |
+
|
| 97 |
+
# ============== SONGS ==============
|
| 98 |
+
|
| 99 |
+
def add_song(title: str, artist: str, album: str = None, duration: int = None,
|
| 100 |
+
bpm: int = None, energy_level: int = None) -> dict:
|
| 101 |
+
"""Add a new song."""
|
| 102 |
+
with get_db_connection() as conn:
|
| 103 |
+
cursor = conn.execute(
|
| 104 |
+
"""INSERT INTO songs (title, artist, album, duration, bpm, energy_level)
|
| 105 |
+
VALUES (?, ?, ?, ?, ?, ?)""",
|
| 106 |
+
(title, artist, album, duration, bpm, energy_level)
|
| 107 |
+
)
|
| 108 |
+
conn.commit()
|
| 109 |
+
song_id = cursor.lastrowid
|
| 110 |
+
return {"id": song_id, "title": title, "artist": artist, "album": album,
|
| 111 |
+
"duration": duration, "bpm": bpm, "energy_level": energy_level}
|
| 112 |
+
|
| 113 |
+
|
| 114 |
+
def get_all_songs() -> list[dict]:
|
| 115 |
+
"""Get all songs."""
|
| 116 |
+
with get_db_connection() as conn:
|
| 117 |
+
cursor = conn.execute("SELECT * FROM songs")
|
| 118 |
+
return [dict(row) for row in cursor.fetchall()]
|
| 119 |
+
|
| 120 |
+
|
| 121 |
+
def get_song_by_id(song_id: int) -> Optional[dict]:
|
| 122 |
+
"""Get a song by ID."""
|
| 123 |
+
with get_db_connection() as conn:
|
| 124 |
+
cursor = conn.execute("SELECT * FROM songs WHERE id = ?", (song_id,))
|
| 125 |
+
row = cursor.fetchone()
|
| 126 |
+
return dict(row) if row else None
|
| 127 |
+
|
| 128 |
+
|
| 129 |
+
def delete_song(song_id: int) -> bool:
|
| 130 |
+
"""Delete a song."""
|
| 131 |
+
with get_db_connection() as conn:
|
| 132 |
+
cursor = conn.execute("DELETE FROM songs WHERE id = ?", (song_id,))
|
| 133 |
+
conn.commit()
|
| 134 |
+
return cursor.rowcount > 0
|
| 135 |
+
|
| 136 |
+
|
| 137 |
+
# ============== USERS ==============
|
| 138 |
+
|
| 139 |
+
def add_user(name: str) -> dict:
|
| 140 |
+
"""Add a new user."""
|
| 141 |
+
with get_db_connection() as conn:
|
| 142 |
+
cursor = conn.execute("INSERT INTO users (name) VALUES (?)", (name,))
|
| 143 |
+
conn.commit()
|
| 144 |
+
user_id = cursor.lastrowid
|
| 145 |
+
cursor = conn.execute("SELECT * FROM users WHERE id = ?", (user_id,))
|
| 146 |
+
return dict(cursor.fetchone())
|
| 147 |
+
|
| 148 |
+
|
| 149 |
+
def get_all_users() -> list[dict]:
|
| 150 |
+
"""Get all users."""
|
| 151 |
+
with get_db_connection() as conn:
|
| 152 |
+
cursor = conn.execute("SELECT * FROM users")
|
| 153 |
+
return [dict(row) for row in cursor.fetchall()]
|
| 154 |
+
|
| 155 |
+
|
| 156 |
+
def get_user_by_id(user_id: int) -> Optional[dict]:
|
| 157 |
+
"""Get a user by ID."""
|
| 158 |
+
with get_db_connection() as conn:
|
| 159 |
+
cursor = conn.execute("SELECT * FROM users WHERE id = ?", (user_id,))
|
| 160 |
+
row = cursor.fetchone()
|
| 161 |
+
return dict(row) if row else None
|
| 162 |
+
|
| 163 |
+
|
| 164 |
+
# ============== PLAYLISTS ==============
|
| 165 |
+
|
| 166 |
+
def add_playlist(name: str, vibe_code: str = None) -> dict:
|
| 167 |
+
"""Add a new playlist."""
|
| 168 |
+
with get_db_connection() as conn:
|
| 169 |
+
cursor = conn.execute(
|
| 170 |
+
"INSERT INTO playlists (name, vibe_code) VALUES (?, ?)",
|
| 171 |
+
(name, vibe_code)
|
| 172 |
+
)
|
| 173 |
+
conn.commit()
|
| 174 |
+
playlist_id = cursor.lastrowid
|
| 175 |
+
cursor = conn.execute("SELECT * FROM playlists WHERE id = ?", (playlist_id,))
|
| 176 |
+
return dict(cursor.fetchone())
|
| 177 |
+
|
| 178 |
+
|
| 179 |
+
def get_all_playlists() -> list[dict]:
|
| 180 |
+
"""Get all playlists."""
|
| 181 |
+
with get_db_connection() as conn:
|
| 182 |
+
cursor = conn.execute("SELECT * FROM playlists")
|
| 183 |
+
return [dict(row) for row in cursor.fetchall()]
|
| 184 |
+
|
| 185 |
+
|
| 186 |
+
def delete_playlist(playlist_id: int) -> bool:
|
| 187 |
+
"""Delete a playlist."""
|
| 188 |
+
with get_db_connection() as conn:
|
| 189 |
+
cursor = conn.execute("DELETE FROM playlists WHERE id = ?", (playlist_id,))
|
| 190 |
+
conn.commit()
|
| 191 |
+
return cursor.rowcount > 0
|
| 192 |
+
|
| 193 |
+
|
| 194 |
+
# ============== MEMORIES ==============
|
| 195 |
+
|
| 196 |
+
def add_memory(user_id: int, description: str, date: str = None, song_id: int = None) -> dict:
|
| 197 |
+
"""Add a new memory."""
|
| 198 |
+
with get_db_connection() as conn:
|
| 199 |
+
cursor = conn.execute(
|
| 200 |
+
"INSERT INTO memories (user_id, description, date, song_id) VALUES (?, ?, ?, ?)",
|
| 201 |
+
(user_id, description, date, song_id)
|
| 202 |
+
)
|
| 203 |
+
conn.commit()
|
| 204 |
+
memory_id = cursor.lastrowid
|
| 205 |
+
cursor = conn.execute("SELECT * FROM memories WHERE id = ?", (memory_id,))
|
| 206 |
+
return dict(cursor.fetchone())
|
| 207 |
+
|
| 208 |
+
|
| 209 |
+
def get_all_memories() -> list[dict]:
|
| 210 |
+
"""Get all memories."""
|
| 211 |
+
with get_db_connection() as conn:
|
| 212 |
+
cursor = conn.execute("SELECT * FROM memories")
|
| 213 |
+
return [dict(row) for row in cursor.fetchall()]
|
| 214 |
+
|
| 215 |
+
|
| 216 |
+
def get_memories_by_user(user_id: int) -> list[dict]:
|
| 217 |
+
"""Get memories for a specific user."""
|
| 218 |
+
with get_db_connection() as conn:
|
| 219 |
+
cursor = conn.execute("SELECT * FROM memories WHERE user_id = ?", (user_id,))
|
| 220 |
+
return [dict(row) for row in cursor.fetchall()]
|
| 221 |
+
|
| 222 |
+
|
| 223 |
+
def delete_memory(memory_id: int) -> bool:
|
| 224 |
+
"""Delete a memory."""
|
| 225 |
+
with get_db_connection() as conn:
|
| 226 |
+
cursor = conn.execute("DELETE FROM memories WHERE id = ?", (memory_id,))
|
| 227 |
+
conn.commit()
|
| 228 |
+
return cursor.rowcount > 0
|
| 229 |
+
|
| 230 |
+
|
| 231 |
+
# ============== CONTEXTS ==============
|
| 232 |
+
|
| 233 |
+
def add_context(weather: str = None, time_of_day: str = None, location_type: str = None) -> dict:
|
| 234 |
+
"""Add a new context."""
|
| 235 |
+
with get_db_connection() as conn:
|
| 236 |
+
cursor = conn.execute(
|
| 237 |
+
"INSERT INTO contexts (weather, time_of_day, location_type) VALUES (?, ?, ?)",
|
| 238 |
+
(weather, time_of_day, location_type)
|
| 239 |
+
)
|
| 240 |
+
conn.commit()
|
| 241 |
+
context_id = cursor.lastrowid
|
| 242 |
+
cursor = conn.execute("SELECT * FROM contexts WHERE id = ?", (context_id,))
|
| 243 |
+
return dict(cursor.fetchone())
|
| 244 |
+
|
| 245 |
+
|
| 246 |
+
def get_all_contexts() -> list[dict]:
|
| 247 |
+
"""Get all contexts."""
|
| 248 |
+
with get_db_connection() as conn:
|
| 249 |
+
cursor = conn.execute("SELECT * FROM contexts")
|
| 250 |
+
return [dict(row) for row in cursor.fetchall()]
|
| 251 |
+
|
| 252 |
+
|
| 253 |
+
def delete_context(context_id: int) -> bool:
|
| 254 |
+
"""Delete a context."""
|
| 255 |
+
with get_db_connection() as conn:
|
| 256 |
+
cursor = conn.execute("DELETE FROM contexts WHERE id = ?", (context_id,))
|
| 257 |
+
conn.commit()
|
| 258 |
+
return cursor.rowcount > 0
|
| 259 |
+
|
| 260 |
+
|
| 261 |
+
# ============== PLAY HISTORY ==============
|
| 262 |
+
|
| 263 |
+
def add_play_history(user_id: int, song_id: int, context_id: int = None) -> dict:
|
| 264 |
+
"""Add a play history entry."""
|
| 265 |
+
played_at = datetime.utcnow().isoformat()
|
| 266 |
+
with get_db_connection() as conn:
|
| 267 |
+
conn.execute(
|
| 268 |
+
"""INSERT INTO play_history (user_id, song_id, played_at, context_id)
|
| 269 |
+
VALUES (?, ?, ?, ?)""",
|
| 270 |
+
(user_id, song_id, played_at, context_id)
|
| 271 |
+
)
|
| 272 |
+
conn.commit()
|
| 273 |
+
return {"user_id": user_id, "song_id": song_id, "played_at": played_at, "context_id": context_id}
|
| 274 |
+
|
| 275 |
+
|
| 276 |
+
def get_play_history(user_id: int = None, limit: int = 50) -> list[dict]:
|
| 277 |
+
"""Get play history, optionally filtered by user."""
|
| 278 |
+
with get_db_connection() as conn:
|
| 279 |
+
if user_id:
|
| 280 |
+
cursor = conn.execute(
|
| 281 |
+
"""SELECT ph.*, s.title, s.artist
|
| 282 |
+
FROM play_history ph
|
| 283 |
+
JOIN songs s ON ph.song_id = s.id
|
| 284 |
+
WHERE ph.user_id = ?
|
| 285 |
+
ORDER BY ph.played_at DESC LIMIT ?""",
|
| 286 |
+
(user_id, limit)
|
| 287 |
+
)
|
| 288 |
+
else:
|
| 289 |
+
cursor = conn.execute(
|
| 290 |
+
"""SELECT ph.*, s.title, s.artist
|
| 291 |
+
FROM play_history ph
|
| 292 |
+
JOIN songs s ON ph.song_id = s.id
|
| 293 |
+
ORDER BY ph.played_at DESC LIMIT ?""",
|
| 294 |
+
(limit,)
|
| 295 |
+
)
|
| 296 |
+
return [dict(row) for row in cursor.fetchall()]
|
main.py
CHANGED
|
@@ -1,22 +1,42 @@
|
|
| 1 |
-
from fastapi import FastAPI
|
|
|
|
| 2 |
import gradio as gr
|
| 3 |
from app import gradio_app
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
|
| 5 |
app = FastAPI()
|
| 6 |
|
| 7 |
-
#
|
| 8 |
-
|
| 9 |
-
{"id": 1, "name": "Laptop", "price": 999.99, "category": "Electronics"},
|
| 10 |
-
{"id": 2, "name": "Headphones", "price": 149.99, "category": "Electronics"},
|
| 11 |
-
{"id": 3, "name": "Coffee Mug", "price": 12.99, "category": "Home"},
|
| 12 |
-
{"id": 4, "name": "Notebook", "price": 5.99, "category": "Office"},
|
| 13 |
-
{"id": 5, "name": "Water Bottle", "price": 24.99, "category": "Home"},
|
| 14 |
-
]
|
| 15 |
|
| 16 |
|
| 17 |
@app.get("/")
|
| 18 |
def greet_json():
|
| 19 |
-
return {"Hello": "World!"}
|
| 20 |
|
| 21 |
|
| 22 |
@app.get("/health")
|
|
@@ -24,10 +44,200 @@ def health_check():
|
|
| 24 |
return {"status": "healthy", "timestamp": "2026-03-30T00:00:00Z"}
|
| 25 |
|
| 26 |
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 30 |
|
| 31 |
|
| 32 |
# Mount the Gradio app at /ui
|
| 33 |
-
app = gr.mount_gradio_app(app, gradio_app, path="/ui")
|
|
|
|
| 1 |
+
from fastapi import FastAPI, Query, HTTPException
|
| 2 |
+
from fastapi.responses import JSONResponse
|
| 3 |
import gradio as gr
|
| 4 |
from app import gradio_app
|
| 5 |
+
from database import (
|
| 6 |
+
init_database,
|
| 7 |
+
# Songs
|
| 8 |
+
add_song, get_all_songs, get_song_by_id, delete_song,
|
| 9 |
+
# Users
|
| 10 |
+
add_user, get_all_users, get_user_by_id,
|
| 11 |
+
# Playlists
|
| 12 |
+
add_playlist, get_all_playlists, delete_playlist,
|
| 13 |
+
# Memories
|
| 14 |
+
add_memory, get_all_memories, get_memories_by_user, delete_memory,
|
| 15 |
+
# Contexts
|
| 16 |
+
add_context, get_all_contexts, delete_context,
|
| 17 |
+
# Play History
|
| 18 |
+
add_play_history, get_play_history,
|
| 19 |
+
)
|
| 20 |
+
from semantic_search import (
|
| 21 |
+
# Song vibes
|
| 22 |
+
add_song_vibe, search_song_vibes, remove_song_vibe,
|
| 23 |
+
# Memory vibes
|
| 24 |
+
add_memory_vibe, search_memory_vibes, remove_memory_vibe,
|
| 25 |
+
# Context vibes
|
| 26 |
+
add_context_vibe, search_context_vibes, remove_context_vibe,
|
| 27 |
+
# Playlist journeys
|
| 28 |
+
add_playlist_journey, search_playlist_journeys, remove_playlist_journey,
|
| 29 |
+
)
|
| 30 |
|
| 31 |
app = FastAPI()
|
| 32 |
|
| 33 |
+
# Initialize SQLite database on startup
|
| 34 |
+
init_database()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 35 |
|
| 36 |
|
| 37 |
@app.get("/")
|
| 38 |
def greet_json():
|
| 39 |
+
return {"Hello": "World!", "app": "Music Memories"}
|
| 40 |
|
| 41 |
|
| 42 |
@app.get("/health")
|
|
|
|
| 44 |
return {"status": "healthy", "timestamp": "2026-03-30T00:00:00Z"}
|
| 45 |
|
| 46 |
|
| 47 |
+
# ============== SONGS ==============
|
| 48 |
+
|
| 49 |
+
@app.get("/songs")
|
| 50 |
+
def list_songs():
|
| 51 |
+
"""Get all songs."""
|
| 52 |
+
return {"songs": get_all_songs()}
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
@app.get("/songs/{song_id}")
|
| 56 |
+
def get_song(song_id: int):
|
| 57 |
+
"""Get a song by ID."""
|
| 58 |
+
song = get_song_by_id(song_id)
|
| 59 |
+
if not song:
|
| 60 |
+
raise HTTPException(status_code=404, detail="Song not found")
|
| 61 |
+
return song
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
@app.post("/songs")
|
| 65 |
+
def create_song(title: str, artist: str, album: str = None, duration: int = None,
|
| 66 |
+
bpm: int = None, energy_level: int = None, lyrics: str = None):
|
| 67 |
+
"""Add a new song."""
|
| 68 |
+
song = add_song(title, artist, album, duration, bpm, energy_level)
|
| 69 |
+
# Add to semantic search index if lyrics provided
|
| 70 |
+
if lyrics:
|
| 71 |
+
add_song_vibe(song["id"], title, artist, lyrics)
|
| 72 |
+
return {"status": "success", "song": song}
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
@app.delete("/songs/{song_id}")
|
| 76 |
+
def delete_song_endpoint(song_id: int):
|
| 77 |
+
"""Delete a song."""
|
| 78 |
+
if not delete_song(song_id):
|
| 79 |
+
raise HTTPException(status_code=404, detail="Song not found")
|
| 80 |
+
remove_song_vibe(song_id)
|
| 81 |
+
return {"status": "success", "deleted_id": song_id}
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
# ============== USERS ==============
|
| 85 |
+
|
| 86 |
+
@app.get("/users")
|
| 87 |
+
def list_users():
|
| 88 |
+
"""Get all users."""
|
| 89 |
+
return {"users": get_all_users()}
|
| 90 |
+
|
| 91 |
+
|
| 92 |
+
@app.get("/users/{user_id}")
|
| 93 |
+
def get_user(user_id: int):
|
| 94 |
+
"""Get a user by ID."""
|
| 95 |
+
user = get_user_by_id(user_id)
|
| 96 |
+
if not user:
|
| 97 |
+
raise HTTPException(status_code=404, detail="User not found")
|
| 98 |
+
return user
|
| 99 |
+
|
| 100 |
+
|
| 101 |
+
@app.post("/users")
|
| 102 |
+
def create_user(name: str):
|
| 103 |
+
"""Add a new user."""
|
| 104 |
+
user = add_user(name)
|
| 105 |
+
return {"status": "success", "user": user}
|
| 106 |
+
|
| 107 |
+
|
| 108 |
+
# ============== PLAYLISTS ==============
|
| 109 |
+
|
| 110 |
+
@app.get("/playlists")
|
| 111 |
+
def list_playlists():
|
| 112 |
+
"""Get all playlists."""
|
| 113 |
+
return {"playlists": get_all_playlists()}
|
| 114 |
+
|
| 115 |
+
|
| 116 |
+
@app.post("/playlists")
|
| 117 |
+
def create_playlist(name: str, vibe_code: str = None, mood_description: str = None):
|
| 118 |
+
"""Add a new playlist."""
|
| 119 |
+
playlist = add_playlist(name, vibe_code)
|
| 120 |
+
# Add to playlist journeys for mood search
|
| 121 |
+
if mood_description:
|
| 122 |
+
add_playlist_journey(playlist["id"], name, mood_description)
|
| 123 |
+
return {"status": "success", "playlist": playlist}
|
| 124 |
+
|
| 125 |
+
|
| 126 |
+
@app.delete("/playlists/{playlist_id}")
|
| 127 |
+
def delete_playlist_endpoint(playlist_id: int):
|
| 128 |
+
"""Delete a playlist."""
|
| 129 |
+
if not delete_playlist(playlist_id):
|
| 130 |
+
raise HTTPException(status_code=404, detail="Playlist not found")
|
| 131 |
+
remove_playlist_journey(playlist_id)
|
| 132 |
+
return {"status": "success", "deleted_id": playlist_id}
|
| 133 |
+
|
| 134 |
+
|
| 135 |
+
# ============== MEMORIES ==============
|
| 136 |
+
|
| 137 |
+
@app.get("/memories")
|
| 138 |
+
def list_memories():
|
| 139 |
+
"""Get all memories."""
|
| 140 |
+
return {"memories": get_all_memories()}
|
| 141 |
+
|
| 142 |
+
|
| 143 |
+
@app.get("/users/{user_id}/memories")
|
| 144 |
+
def list_user_memories(user_id: int):
|
| 145 |
+
"""Get memories for a specific user."""
|
| 146 |
+
if not get_user_by_id(user_id):
|
| 147 |
+
raise HTTPException(status_code=404, detail="User not found")
|
| 148 |
+
return {"memories": get_memories_by_user(user_id)}
|
| 149 |
+
|
| 150 |
+
|
| 151 |
+
@app.post("/memories")
|
| 152 |
+
def create_memory(user_id: int, description: str, date: str = None, song_id: int = None):
|
| 153 |
+
"""Add a new memory."""
|
| 154 |
+
if not get_user_by_id(user_id):
|
| 155 |
+
raise HTTPException(status_code=404, detail="User not found")
|
| 156 |
+
memory = add_memory(user_id, description, date, song_id)
|
| 157 |
+
# Add to semantic search index
|
| 158 |
+
add_memory_vibe(memory["id"], user_id, description)
|
| 159 |
+
return {"status": "success", "memory": memory}
|
| 160 |
+
|
| 161 |
+
|
| 162 |
+
@app.delete("/memories/{memory_id}")
|
| 163 |
+
def delete_memory_endpoint(memory_id: int):
|
| 164 |
+
"""Delete a memory."""
|
| 165 |
+
if not delete_memory(memory_id):
|
| 166 |
+
raise HTTPException(status_code=404, detail="Memory not found")
|
| 167 |
+
remove_memory_vibe(memory_id)
|
| 168 |
+
return {"status": "success", "deleted_id": memory_id}
|
| 169 |
+
|
| 170 |
+
|
| 171 |
+
# ============== CONTEXTS ==============
|
| 172 |
+
|
| 173 |
+
@app.get("/contexts")
|
| 174 |
+
def list_contexts():
|
| 175 |
+
"""Get all contexts."""
|
| 176 |
+
return {"contexts": get_all_contexts()}
|
| 177 |
+
|
| 178 |
+
|
| 179 |
+
@app.post("/contexts")
|
| 180 |
+
def create_context(weather: str = None, time_of_day: str = None, location_type: str = None):
|
| 181 |
+
"""Add a new context."""
|
| 182 |
+
context = add_context(weather, time_of_day, location_type)
|
| 183 |
+
# Add to semantic search index
|
| 184 |
+
add_context_vibe(context["id"], weather or "", time_of_day or "", location_type or "")
|
| 185 |
+
return {"status": "success", "context": context}
|
| 186 |
+
|
| 187 |
+
|
| 188 |
+
@app.delete("/contexts/{context_id}")
|
| 189 |
+
def delete_context_endpoint(context_id: int):
|
| 190 |
+
"""Delete a context."""
|
| 191 |
+
if not delete_context(context_id):
|
| 192 |
+
raise HTTPException(status_code=404, detail="Context not found")
|
| 193 |
+
remove_context_vibe(context_id)
|
| 194 |
+
return {"status": "success", "deleted_id": context_id}
|
| 195 |
+
|
| 196 |
+
|
| 197 |
+
# ============== PLAY HISTORY ==============
|
| 198 |
+
|
| 199 |
+
@app.get("/history")
|
| 200 |
+
def list_history(user_id: int = None, limit: int = 50):
|
| 201 |
+
"""Get play history."""
|
| 202 |
+
return {"history": get_play_history(user_id, limit)}
|
| 203 |
+
|
| 204 |
+
|
| 205 |
+
@app.post("/history")
|
| 206 |
+
def create_history(user_id: int, song_id: int, context_id: int = None):
|
| 207 |
+
"""Add a play history entry."""
|
| 208 |
+
history = add_play_history(user_id, song_id, context_id)
|
| 209 |
+
return {"status": "success", "history": history}
|
| 210 |
+
|
| 211 |
+
|
| 212 |
+
# ============== SEMANTIC SEARCH ==============
|
| 213 |
+
|
| 214 |
+
@app.get("/search/songs")
|
| 215 |
+
def search_songs(q: str = Query(..., description="Search query"), n: int = Query(5, description="Number of results")):
|
| 216 |
+
"""Semantic search for songs by vibe/lyrics."""
|
| 217 |
+
results = search_song_vibes(q, n_results=n)
|
| 218 |
+
return {"query": q, "results": results}
|
| 219 |
+
|
| 220 |
+
|
| 221 |
+
@app.get("/search/memories")
|
| 222 |
+
def search_memories(q: str = Query(..., description="Search query"), n: int = Query(5, description="Number of results")):
|
| 223 |
+
"""Semantic search for memories."""
|
| 224 |
+
results = search_memory_vibes(q, n_results=n)
|
| 225 |
+
return {"query": q, "results": results}
|
| 226 |
+
|
| 227 |
+
|
| 228 |
+
@app.get("/search/contexts")
|
| 229 |
+
def search_contexts(q: str = Query(..., description="Search query"), n: int = Query(5, description="Number of results")):
|
| 230 |
+
"""Semantic search for contexts."""
|
| 231 |
+
results = search_context_vibes(q, n_results=n)
|
| 232 |
+
return {"query": q, "results": results}
|
| 233 |
+
|
| 234 |
+
|
| 235 |
+
@app.get("/search/playlists")
|
| 236 |
+
def search_playlists(q: str = Query(..., description="Search query"), n: int = Query(5, description="Number of results")):
|
| 237 |
+
"""Semantic search for playlists by mood."""
|
| 238 |
+
results = search_playlist_journeys(q, n_results=n)
|
| 239 |
+
return {"query": q, "results": results}
|
| 240 |
|
| 241 |
|
| 242 |
# Mount the Gradio app at /ui
|
| 243 |
+
app = gr.mount_gradio_app(app, gradio_app, path="/ui")
|
pyproject.toml
CHANGED
|
@@ -7,11 +7,13 @@ requires-python = ">=3.12"
|
|
| 7 |
dependencies = [
|
| 8 |
"accelerate>=1.13.0",
|
| 9 |
"bitsandbytes>=0.49.2; sys_platform == 'linux' and platform_machine == 'x86_64'",
|
|
|
|
| 10 |
"datasets>=4.8.4",
|
| 11 |
"fastapi>=0.135.2",
|
| 12 |
"gradio>=4.0.0",
|
| 13 |
"huggingface-hub>=1.8.0",
|
| 14 |
"peft>=0.18.1",
|
|
|
|
| 15 |
"torch>=2.5.0",
|
| 16 |
"torchaudio>=2.5.0",
|
| 17 |
"transformers>=5.4.0",
|
|
|
|
| 7 |
dependencies = [
|
| 8 |
"accelerate>=1.13.0",
|
| 9 |
"bitsandbytes>=0.49.2; sys_platform == 'linux' and platform_machine == 'x86_64'",
|
| 10 |
+
"chromadb>=0.5.0",
|
| 11 |
"datasets>=4.8.4",
|
| 12 |
"fastapi>=0.135.2",
|
| 13 |
"gradio>=4.0.0",
|
| 14 |
"huggingface-hub>=1.8.0",
|
| 15 |
"peft>=0.18.1",
|
| 16 |
+
"sentence-transformers>=3.0.0",
|
| 17 |
"torch>=2.5.0",
|
| 18 |
"torchaudio>=2.5.0",
|
| 19 |
"transformers>=5.4.0",
|
seed_data.json
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Initial products for seeding the database
|
| 2 |
+
[
|
| 3 |
+
{"name": "Laptop", "price": 999.99, "category": "Electronics"},
|
| 4 |
+
{"name": "Headphones", "price": 149.99, "category": "Electronics"},
|
| 5 |
+
{"name": "Coffee Mug", "price": 12.99, "category": "Home"},
|
| 6 |
+
{"name": "Notebook", "price": 5.99, "category": "Office"},
|
| 7 |
+
{"name": "Water Bottle", "price": 24.99, "category": "Home"}
|
| 8 |
+
]
|
seed_db.py
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Seed the database with initial data for consistent Docker builds."""
|
| 3 |
+
|
| 4 |
+
import json
|
| 5 |
+
import os
|
| 6 |
+
from database import (
|
| 7 |
+
init_database,
|
| 8 |
+
add_song, add_user, add_playlist, add_memory, add_context,
|
| 9 |
+
get_all_songs, get_all_users
|
| 10 |
+
)
|
| 11 |
+
from semantic_search import (
|
| 12 |
+
add_song_vibe, add_memory_vibe, add_context_vibe, add_playlist_journey
|
| 13 |
+
)
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
def seed_database():
|
| 17 |
+
"""Initialize and seed the database with sample data."""
|
| 18 |
+
|
| 19 |
+
# Initialize SQLite tables
|
| 20 |
+
print("📦 Initializing database...")
|
| 21 |
+
init_database()
|
| 22 |
+
|
| 23 |
+
# Check if already seeded
|
| 24 |
+
if get_all_songs():
|
| 25 |
+
print("✅ Database already seeded, skipping...")
|
| 26 |
+
return
|
| 27 |
+
|
| 28 |
+
print("🌱 Seeding database...")
|
| 29 |
+
|
| 30 |
+
# ============== SONGS ==============
|
| 31 |
+
songs_data = [
|
| 32 |
+
{"title": "Bohemian Rhapsody", "artist": "Queen", "album": "A Night at the Opera", "duration": 354, "bpm": 72, "energy_level": 8, "lyrics": "Is this the real life? Is this just fantasy? Caught in a landslide, no escape from reality"},
|
| 33 |
+
{"title": "Imagine", "artist": "John Lennon", "album": "Imagine", "duration": 183, "bpm": 75, "energy_level": 3, "lyrics": "Imagine there's no heaven, it's easy if you try, no hell below us, above us only sky"},
|
| 34 |
+
{"title": "Blinding Lights", "artist": "The Weeknd", "album": "After Hours", "duration": 200, "bpm": 171, "energy_level": 9, "lyrics": "I've been on my own for long enough, maybe you can show me how to love, maybe"},
|
| 35 |
+
{"title": "Someone Like You", "artist": "Adele", "album": "21", "duration": 285, "bpm": 67, "energy_level": 4, "lyrics": "I heard that you're settled down, that you found a girl and you're married now"},
|
| 36 |
+
{"title": "Uptown Funk", "artist": "Mark Ronson ft. Bruno Mars", "album": "Uptown Special", "duration": 269, "bpm": 115, "energy_level": 10, "lyrics": "This hit, that ice cold, Michelle Pfeiffer, that white gold"},
|
| 37 |
+
{"title": "Fix You", "artist": "Coldplay", "album": "X&Y", "duration": 295, "bpm": 138, "energy_level": 5, "lyrics": "When you try your best but you don't succeed, when you get what you want but not what you need"},
|
| 38 |
+
{"title": "Lose Yourself", "artist": "Eminem", "album": "8 Mile", "duration": 326, "bpm": 86, "energy_level": 9, "lyrics": "Look, if you had one shot or one opportunity, to seize everything you ever wanted"},
|
| 39 |
+
{"title": "Let It Be", "artist": "The Beatles", "album": "Let It Be", "duration": 243, "bpm": 75, "energy_level": 3, "lyrics": "When I find myself in times of trouble, Mother Mary comes to me"},
|
| 40 |
+
{"title": "Rolling in the Deep", "artist": "Adele", "album": "21", "duration": 228, "bpm": 105, "energy_level": 7, "lyrics": "There's a fire starting in my heart, I'm reaching for every high and low"},
|
| 41 |
+
{"title": "Happy", "artist": "Pharrell Williams", "album": "G I R L", "duration": 232, "bpm": 160, "energy_level": 10, "lyrics": "It might seem crazy what I'm about to say, sunshine she's here, you can take a guess"},
|
| 42 |
+
]
|
| 43 |
+
|
| 44 |
+
print("🎵 Adding songs...")
|
| 45 |
+
for song in songs_data:
|
| 46 |
+
lyrics = song.pop("lyrics")
|
| 47 |
+
added_song = add_song(**song)
|
| 48 |
+
add_song_vibe(added_song["id"], song["title"], song["artist"], lyrics)
|
| 49 |
+
|
| 50 |
+
# ============== USERS ==============
|
| 51 |
+
users_data = ["Alice", "Bob", "Charlie", "Diana", "Eve"]
|
| 52 |
+
|
| 53 |
+
print("👤 Adding users...")
|
| 54 |
+
for name in users_data:
|
| 55 |
+
add_user(name)
|
| 56 |
+
|
| 57 |
+
# ============== PLAYLISTS ==============
|
| 58 |
+
playlists_data = [
|
| 59 |
+
{"name": "Rainy Day Vibes", "vibe_code": "chill_melancholy", "mood": "Perfect for cloudy afternoons when you want to feel your feelings"},
|
| 60 |
+
{"name": "Workout Energy", "vibe_code": "high_energy", "mood": "Maximum pump-up tracks for crushing your fitness goals"},
|
| 61 |
+
{"name": "Late Night Coding", "vibe_code": "focus_calm", "mood": "Deep focus music for those 3am debugging sessions"},
|
| 62 |
+
{"name": "Road Trip Classics", "vibe_code": "adventure_fun", "mood": "Windows down, volume up, endless highway ahead"},
|
| 63 |
+
{"name": "Heartbreak Recovery", "vibe_code": "sad_to_healing", "mood": "Journey from tears to acceptance, one song at a time"},
|
| 64 |
+
]
|
| 65 |
+
|
| 66 |
+
print("🎵 Adding playlists...")
|
| 67 |
+
for pl in playlists_data:
|
| 68 |
+
mood = pl.pop("mood")
|
| 69 |
+
added_pl = add_playlist(**pl)
|
| 70 |
+
add_playlist_journey(added_pl["id"], pl["name"], mood)
|
| 71 |
+
|
| 72 |
+
# ============== CONTEXTS ==============
|
| 73 |
+
contexts_data = [
|
| 74 |
+
{"weather": "rainy", "time_of_day": "night", "location_type": "home"},
|
| 75 |
+
{"weather": "sunny", "time_of_day": "morning", "location_type": "car"},
|
| 76 |
+
{"weather": "cloudy", "time_of_day": "afternoon", "location_type": "office"},
|
| 77 |
+
{"weather": "clear", "time_of_day": "evening", "location_type": "gym"},
|
| 78 |
+
{"weather": "stormy", "time_of_day": "night", "location_type": "home"},
|
| 79 |
+
]
|
| 80 |
+
|
| 81 |
+
print("🌤️ Adding contexts...")
|
| 82 |
+
for ctx in contexts_data:
|
| 83 |
+
added_ctx = add_context(**ctx)
|
| 84 |
+
add_context_vibe(added_ctx["id"], ctx.get("weather", ""), ctx.get("time_of_day", ""), ctx.get("location_type", ""))
|
| 85 |
+
|
| 86 |
+
# ============== MEMORIES ==============
|
| 87 |
+
memories_data = [
|
| 88 |
+
{"user_id": 1, "description": "First dance at my wedding, perfect song for the perfect moment", "date": "2024-06-15"},
|
| 89 |
+
{"user_id": 1, "description": "Road trip to the coast with friends, singing at the top of our lungs", "date": "2024-07-20"},
|
| 90 |
+
{"user_id": 2, "description": "Studying for finals, this song kept me going through the night", "date": "2024-03-10"},
|
| 91 |
+
{"user_id": 2, "description": "Running my first 5K, this was my power song at the finish line", "date": "2024-05-01"},
|
| 92 |
+
{"user_id": 3, "description": "Grandma's funeral, this song made everyone cry but also smile", "date": "2023-11-30"},
|
| 93 |
+
]
|
| 94 |
+
|
| 95 |
+
print("💭 Adding memories...")
|
| 96 |
+
for mem in memories_data:
|
| 97 |
+
added_mem = add_memory(**mem)
|
| 98 |
+
add_memory_vibe(added_mem["id"], mem["user_id"], mem["description"])
|
| 99 |
+
|
| 100 |
+
print("\n✅ Database seeded successfully!")
|
| 101 |
+
print(f" - {len(songs_data)} songs")
|
| 102 |
+
print(f" - {len(users_data)} users")
|
| 103 |
+
print(f" - {len(playlists_data)} playlists")
|
| 104 |
+
print(f" - {len(contexts_data)} contexts")
|
| 105 |
+
print(f" - {len(memories_data)} memories")
|
| 106 |
+
|
| 107 |
+
|
| 108 |
+
if __name__ == "__main__":
|
| 109 |
+
seed_database()
|
semantic_search.py
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""ChromaDB semantic search module for music memories app."""
|
| 2 |
+
|
| 3 |
+
import chromadb
|
| 4 |
+
from sentence_transformers import SentenceTransformer
|
| 5 |
+
|
| 6 |
+
# Initialize the embedding model
|
| 7 |
+
embedding_model = SentenceTransformer("all-MiniLM-L6-v2")
|
| 8 |
+
|
| 9 |
+
# Initialize ChromaDB client with persistent storage
|
| 10 |
+
chroma_client = chromadb.PersistentClient(path="./chroma_db")
|
| 11 |
+
|
| 12 |
+
# Create collections for different semantic search types
|
| 13 |
+
song_vibes_collection = chroma_client.get_or_create_collection(name="song_vibes")
|
| 14 |
+
memory_vibes_collection = chroma_client.get_or_create_collection(name="memory_vibes")
|
| 15 |
+
context_vibes_collection = chroma_client.get_or_create_collection(name="context_vibes")
|
| 16 |
+
playlist_journeys_collection = chroma_client.get_or_create_collection(name="playlist_journeys")
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
# ============== SONG VIBES ==============
|
| 20 |
+
|
| 21 |
+
def add_song_vibe(song_id: int, title: str, artist: str, lyrics: str = "") -> None:
|
| 22 |
+
"""Add song embedding based on lyrics/title/artist."""
|
| 23 |
+
text = f"{title} by {artist} - {lyrics}".strip()
|
| 24 |
+
song_vibes_collection.add(
|
| 25 |
+
ids=[f"song_{song_id}"],
|
| 26 |
+
documents=[text],
|
| 27 |
+
metadatas=[{"id": song_id, "title": title, "artist": artist}],
|
| 28 |
+
)
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
def search_song_vibes(query: str, n_results: int = 5) -> list[dict]:
|
| 32 |
+
"""Search songs by vibe/lyrics."""
|
| 33 |
+
results = song_vibes_collection.query(
|
| 34 |
+
query_texts=[query],
|
| 35 |
+
n_results=n_results,
|
| 36 |
+
include=["documents", "metadatas", "distances"],
|
| 37 |
+
)
|
| 38 |
+
if not results["metadatas"] or not results["metadatas"][0]:
|
| 39 |
+
return []
|
| 40 |
+
return [
|
| 41 |
+
{"id": m["id"], "title": m["title"], "artist": m["artist"],
|
| 42 |
+
"document": results["documents"][0][i], "distance": results["distances"][0][i]}
|
| 43 |
+
for i, m in enumerate(results["metadatas"][0])
|
| 44 |
+
]
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
def remove_song_vibe(song_id: int) -> None:
|
| 48 |
+
"""Remove a song vibe."""
|
| 49 |
+
song_vibes_collection.delete(ids=[f"song_{song_id}"])
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
# ============== MEMORY VIBES ==============
|
| 53 |
+
|
| 54 |
+
def add_memory_vibe(memory_id: int, user_id: int, description: str) -> None:
|
| 55 |
+
"""Add memory embedding based on description."""
|
| 56 |
+
memory_vibes_collection.add(
|
| 57 |
+
ids=[f"memory_{memory_id}"],
|
| 58 |
+
documents=[description],
|
| 59 |
+
metadatas=[{"id": memory_id, "user_id": user_id}],
|
| 60 |
+
)
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
def search_memory_vibes(query: str, n_results: int = 5) -> list[dict]:
|
| 64 |
+
"""Search memories by description."""
|
| 65 |
+
results = memory_vibes_collection.query(
|
| 66 |
+
query_texts=[query],
|
| 67 |
+
n_results=n_results,
|
| 68 |
+
include=["documents", "metadatas", "distances"],
|
| 69 |
+
)
|
| 70 |
+
if not results["metadatas"] or not results["metadatas"][0]:
|
| 71 |
+
return []
|
| 72 |
+
return [
|
| 73 |
+
{"id": m["id"], "user_id": m["user_id"],
|
| 74 |
+
"document": results["documents"][0][i], "distance": results["distances"][0][i]}
|
| 75 |
+
for i, m in enumerate(results["metadatas"][0])
|
| 76 |
+
]
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
def remove_memory_vibe(memory_id: int) -> None:
|
| 80 |
+
"""Remove a memory vibe."""
|
| 81 |
+
memory_vibes_collection.delete(ids=[f"memory_{memory_id}"])
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
# ============== CONTEXT VIBES ==============
|
| 85 |
+
|
| 86 |
+
def add_context_vibe(context_id: int, weather: str, time_of_day: str, location_type: str) -> None:
|
| 87 |
+
"""Add context embedding."""
|
| 88 |
+
text = f"Weather: {weather}, Time: {time_of_day}, Location: {location_type}".strip()
|
| 89 |
+
context_vibes_collection.add(
|
| 90 |
+
ids=[f"context_{context_id}"],
|
| 91 |
+
documents=[text],
|
| 92 |
+
metadatas=[{"id": context_id, "weather": weather, "time_of_day": time_of_day, "location_type": location_type}],
|
| 93 |
+
)
|
| 94 |
+
|
| 95 |
+
|
| 96 |
+
def search_context_vibes(query: str, n_results: int = 5) -> list[dict]:
|
| 97 |
+
"""Search contexts by description."""
|
| 98 |
+
results = context_vibes_collection.query(
|
| 99 |
+
query_texts=[query],
|
| 100 |
+
n_results=n_results,
|
| 101 |
+
include=["documents", "metadatas", "distances"],
|
| 102 |
+
)
|
| 103 |
+
if not results["metadatas"] or not results["metadatas"][0]:
|
| 104 |
+
return []
|
| 105 |
+
return [
|
| 106 |
+
{"id": m["id"], "weather": m["weather"], "time_of_day": m["time_of_day"],
|
| 107 |
+
"location_type": m["location_type"], "document": results["documents"][0][i],
|
| 108 |
+
"distance": results["distances"][0][i]}
|
| 109 |
+
for i, m in enumerate(results["metadatas"][0])
|
| 110 |
+
]
|
| 111 |
+
|
| 112 |
+
|
| 113 |
+
def remove_context_vibe(context_id: int) -> None:
|
| 114 |
+
"""Remove a context vibe."""
|
| 115 |
+
context_vibes_collection.delete(ids=[f"context_{context_id}"])
|
| 116 |
+
|
| 117 |
+
|
| 118 |
+
# ============== PLAYLIST JOURNEYS ==============
|
| 119 |
+
|
| 120 |
+
def add_playlist_journey(playlist_id: int, name: str, mood_description: str) -> None:
|
| 121 |
+
"""Add playlist journey embedding for mood transitions."""
|
| 122 |
+
playlist_journeys_collection.add(
|
| 123 |
+
ids=[f"playlist_{playlist_id}"],
|
| 124 |
+
documents=[f"{name}: {mood_description}"],
|
| 125 |
+
metadatas=[{"id": playlist_id, "name": name}],
|
| 126 |
+
)
|
| 127 |
+
|
| 128 |
+
|
| 129 |
+
def search_playlist_journeys(query: str, n_results: int = 5) -> list[dict]:
|
| 130 |
+
"""Search playlists by mood/vibe."""
|
| 131 |
+
results = playlist_journeys_collection.query(
|
| 132 |
+
query_texts=[query],
|
| 133 |
+
n_results=n_results,
|
| 134 |
+
include=["documents", "metadatas", "distances"],
|
| 135 |
+
)
|
| 136 |
+
if not results["metadatas"] or not results["metadatas"][0]:
|
| 137 |
+
return []
|
| 138 |
+
return [
|
| 139 |
+
{"id": m["id"], "name": m["name"],
|
| 140 |
+
"document": results["documents"][0][i], "distance": results["distances"][0][i]}
|
| 141 |
+
for i, m in enumerate(results["metadatas"][0])
|
| 142 |
+
]
|
| 143 |
+
|
| 144 |
+
|
| 145 |
+
def remove_playlist_journey(playlist_id: int) -> None:
|
| 146 |
+
"""Remove a playlist journey."""
|
| 147 |
+
playlist_journeys_collection.delete(ids=[f"playlist_{playlist_id}"])
|
uv.lock
CHANGED
|
The diff for this file is too large to render.
See raw diff
|
|
|