-
-
Notifications
You must be signed in to change notification settings - Fork 140
Expand file tree
/
Copy pathapp_clap_search.py
More file actions
365 lines (322 loc) · 10.4 KB
/
Copy pathapp_clap_search.py
File metadata and controls
365 lines (322 loc) · 10.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
"""
CLAP Text Search Blueprint
Provides web interface and API for natural language music search.
"""
from flask import Blueprint, render_template, request, jsonify
import logging
logger = logging.getLogger(__name__)
clap_search_bp = Blueprint('clap_search_bp', __name__, template_folder='../templates')
@clap_search_bp.route('/clap_search', methods=['GET'])
def clap_search_page():
"""
CLAP text search UI page.
---
tags:
- CLAP Search
summary: HTML page for natural-language music search powered by CLAP audio↔text embeddings.
responses:
200:
description: HTML page rendered.
"""
from config import CLAP_ENABLED, APP_VERSION
from tasks.clap_text_search import get_cache_stats
cache_stats = get_cache_stats()
return render_template(
'clap_search.html',
title='Text Search - AudioMuse-AI',
active='clap_search',
app_version=APP_VERSION,
clap_enabled=CLAP_ENABLED,
cache_stats=cache_stats
)
@clap_search_bp.route('/api/clap/search', methods=['POST'])
def clap_search_api():
"""
Run a CLAP text-to-audio similarity search.
---
tags:
- CLAP Search
summary: Return the top-N tracks whose CLAP audio embedding best matches a free-text query.
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [query]
properties:
query:
type: string
minLength: 3
example: "upbeat summer songs"
limit:
type: integer
minimum: 1
maximum: 500
default: 100
responses:
200:
description: Search results sorted by descending similarity.
content:
application/json:
schema:
type: object
properties:
query:
type: string
count:
type: integer
results:
type: array
items:
type: object
properties:
item_id:
type: string
title:
type: string
author:
type: string
similarity:
type: number
format: float
400:
description: CLAP disabled, missing query, or query too short.
500:
description: Internal error during search.
503:
description: CLAP cache not loaded yet (run analysis first).
"""
from config import CLAP_ENABLED
from tasks.clap_text_search import search_by_text, is_clap_cache_loaded
from app_helper import attach_song_features
if not CLAP_ENABLED:
return jsonify({
'error': 'CLAP text search is disabled. Set CLAP_ENABLED=true in config.',
'results': []
}), 400
try:
data = request.get_json()
if not data or 'query' not in data:
return jsonify({'error': 'Missing "query" in request body'}), 400
query = data['query'].strip()
limit = data.get('limit', 100)
if not query:
return jsonify({'error': 'Query cannot be empty'}), 400
if len(query) < 1:
return jsonify({'error': 'Query must be at least 1 character'}), 400
# Validate limit
limit = min(max(1, int(limit)), 500) # Between 1 and 500
# Check if cache is loaded
if not is_clap_cache_loaded():
return jsonify({
'error': 'CLAP cache not loaded. Please run song analysis first.',
'results': []
}), 503
# Perform search
results = search_by_text(query, limit=limit)
attach_song_features(results)
return jsonify({
'query': query,
'results': results,
'count': len(results)
})
except ValueError as e:
logger.warning(f"ValueError in DCLAP search API: {e}")
return jsonify({'error': 'Invalid or missing request parameter.'}), 400
except Exception as e:
logger.exception(f"DCLAP search API error: {e}")
return jsonify({'error': 'An internal server error occurred during DCLAP search.'}), 500
@clap_search_bp.route('/api/clap/warmup', methods=['POST'])
def warmup_model_api():
"""
Warm up the CLAP text-search model.
---
tags:
- CLAP Search
summary: Preload the DCLAP model and reset the 10-minute idle-eviction timer.
description: Call this when the search page loads to ensure subsequent queries are fast.
responses:
200:
description: Model loaded; cache timer reset.
content:
application/json:
schema:
type: object
properties:
loaded:
type: boolean
expiry_seconds:
type: integer
example: 600
400:
description: CLAP search is disabled.
500:
description: Warmup failed.
"""
from config import CLAP_ENABLED
from tasks.clap_text_search import warmup_text_search_model
if not CLAP_ENABLED:
return jsonify({
'error': 'CLAP text search is disabled',
'loaded': False
}), 400
try:
status = warmup_text_search_model()
return jsonify(status)
except Exception:
logger.exception("Model warmup failed")
return jsonify({
'error': 'Warmup failed.',
'loaded': False
}), 500
@clap_search_bp.route('/api/clap/warmup/status', methods=['GET'])
def warmup_status_api():
"""
CLAP warmup status.
---
tags:
- CLAP Search
summary: Return whether the warm cache is still active and how long until it expires.
responses:
200:
description: Warm cache state.
content:
application/json:
schema:
type: object
properties:
active:
type: boolean
seconds_remaining:
type: integer
"""
from config import CLAP_ENABLED
from tasks.clap_text_search import get_warm_cache_status
if not CLAP_ENABLED:
return jsonify({'active': False, 'seconds_remaining': 0})
try:
status = get_warm_cache_status()
return jsonify(status)
except Exception as e:
logger.error(f"Failed to get warmup status: {e}")
return jsonify({'active': False, 'seconds_remaining': 0})
@clap_search_bp.route('/api/clap/cache/refresh', methods=['POST'])
def refresh_cache_api():
"""
Refresh the CLAP audio-embedding cache.
---
tags:
- CLAP Search
summary: Reload the CLAP cache from the database (call after analysis completes).
responses:
200:
description: Cache refreshed; updated stats returned.
content:
application/json:
schema:
type: object
properties:
success:
type: boolean
message:
type: string
stats:
type: object
400:
description: CLAP disabled.
500:
description: Refresh failed.
"""
from config import CLAP_ENABLED
from tasks.clap_text_search import refresh_clap_cache, get_cache_stats
if not CLAP_ENABLED:
return jsonify({'error': 'CLAP is disabled'}), 400
try:
success = refresh_clap_cache()
stats = get_cache_stats()
if success:
return jsonify({
'success': True,
'message': 'CLAP cache refreshed successfully',
'stats': stats
})
else:
return jsonify({
'success': False,
'message': 'Failed to refresh CLAP cache',
'stats': stats
}), 500
except Exception as e:
logger.error(f"Cache refresh failed: {e}")
return jsonify({
'success': False,
'error': 'An internal error occurred. Please try again later.'
}), 500
@clap_search_bp.route('/api/clap/stats', methods=['GET'])
def cache_stats_api():
"""
CLAP cache stats.
---
tags:
- CLAP Search
summary: Return cache size, freshness, and the CLAP_ENABLED flag.
responses:
200:
description: Cache statistics.
content:
application/json:
schema:
type: object
properties:
clap_enabled:
type: boolean
num_embeddings:
type: integer
last_refresh:
type: string
"""
from config import CLAP_ENABLED
from tasks.clap_text_search import get_cache_stats
stats = get_cache_stats()
stats['clap_enabled'] = CLAP_ENABLED
return jsonify(stats)
@clap_search_bp.route('/api/clap/top_queries', methods=['GET'])
def top_queries_api():
"""
Top diverse CLAP queries.
---
tags:
- CLAP Search
summary: Return the precomputed top-50 diverse search queries shown as suggestions.
description: |
Returns an empty list with `ready: false` if the background diversity
computation hasn't finished yet.
responses:
200:
description: Suggested queries.
content:
application/json:
schema:
type: object
properties:
queries:
type: array
items:
type: string
ready:
type: boolean
"""
from config import CLAP_ENABLED
from tasks.clap_text_search import get_cached_top_queries
if not CLAP_ENABLED:
return jsonify({'queries': [], 'ready': False, 'message': 'CLAP disabled'}), 200
try:
queries = get_cached_top_queries()
return jsonify({
'queries': queries,
'ready': len(queries) > 0
}), 200
except Exception as e:
logger.exception("Failed to get top queries")
return jsonify({'error': 'An internal error occurred. Please try again later.', 'queries': [], 'ready': False}), 500