Ubuntu commited on
Commit
8c9a0f8
·
1 Parent(s): 9af0669

workign 2 dbs need toe be in server

Browse files
Files changed (12) hide show
  1. .dockerignore +73 -0
  2. .gitignore +1 -0
  3. Dockerfile +8 -0
  4. README.md +120 -10
  5. app.py +226 -39
  6. database.py +296 -0
  7. main.py +224 -14
  8. pyproject.toml +2 -0
  9. seed_data.json +8 -0
  10. seed_db.py +109 -0
  11. semantic_search.py +147 -0
  12. 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
- title: Pool
3
- emoji: 🌍
4
- colorFrom: blue
5
- colorTo: yellow
6
- sdk: docker
7
- pinned: false
8
- ---
9
-
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 that calls the FastAPI backend."""
9
-
10
- def greet(name):
11
- """Call the greeting endpoint."""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
12
  try:
13
  with httpx.Client() as client:
14
- resp = client.get(f"{BASE_URL}/", timeout=5.0)
 
 
 
 
 
 
 
 
 
 
 
 
 
15
  resp.raise_for_status()
16
  data = resp.json()
17
- return f"{data['Hello']}, {name}! Welcome to Pool API."
 
 
18
  except Exception as e:
19
- return f"Error: {str(e)}"
20
 
21
- def load_products():
22
- """Call the products endpoint."""
23
  try:
24
  with httpx.Client() as client:
25
- resp = client.get(f"{BASE_URL}/products", timeout=5.0)
26
  resp.raise_for_status()
27
  data = resp.json()
28
- return [[p["id"], p["name"], p["price"], p["category"]] for p in data["products"]]
 
 
29
  except Exception as e:
30
- return [["Error", str(e), "", ""]]
31
 
32
- def check_health():
33
- """Call the health endpoint."""
34
  try:
35
  with httpx.Client() as client:
36
- resp = client.get(f"{BASE_URL}/health", timeout=5.0)
37
  resp.raise_for_status()
38
- return resp.json()
 
 
 
39
  except Exception as e:
40
- return {"error": str(e)}
41
-
42
- with gr.Blocks(title="Pool UI") as demo:
43
- gr.Markdown("# 🌍 Pool API - Gradio UI")
44
- gr.Markdown("Interact with the Pool API endpoints below.")
45
-
46
- with gr.Tab("Greeting"):
47
- name_input = gr.Textbox(label="Enter your name", placeholder="Type your name here...")
48
- greet_btn = gr.Button("Greet")
49
- greet_output = gr.Textbox(label="Response")
50
- greet_btn.click(fn=greet, inputs=name_input, outputs=greet_output)
51
-
52
- with gr.Tab("Products"):
53
- products_table = gr.Dataframe(
54
- headers=["ID", "Name", "Price", "Category"],
55
- datatype=["number", "str", "number", "str"],
56
- label="Product List"
57
- )
58
- load_btn = gr.Button("Load Products")
59
- load_btn.click(fn=load_products, outputs=products_table)
60
-
61
- with gr.Tab("Health Check"):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
62
  health_output = gr.JSON(label="Health Status")
63
  health_btn = gr.Button("Check Health")
64
- health_btn.click(fn=check_health, outputs=health_output)
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
- # Static product data
8
- PRODUCTS = [
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
- @app.get("/products")
28
- def get_products():
29
- return {"products": PRODUCTS}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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