-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathapi.py
More file actions
128 lines (109 loc) · 3.65 KB
/
Copy pathapi.py
File metadata and controls
128 lines (109 loc) · 3.65 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
import base64
import json
import logging
import time
from pathlib import Path
from uuid import uuid4
from fastapi import FastAPI, HTTPException
from fastapi.responses import StreamingResponse
from pydantic import BaseModel, Field
from vector_db import SongVectorDB
DB_PATH = Path("lyriccovers_output/songs.db")
TOP_K_DEFAULT = 20
try:
VECTOR_DB = SongVectorDB(DB_PATH)
except FileNotFoundError:
raise RuntimeError(
f"SQLite database not found at {DB_PATH}. Create and populate it before starting the API."
) from None
app = FastAPI(title="LyricCovers Vector API", version="1.0")
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [%(levelname)s] %(message)s",
)
logger = logging.getLogger("lyriccovers.api")
class SearchRequest(BaseModel):
query: str = Field(..., min_length=1)
top_k: int = Field(TOP_K_DEFAULT, ge=1)
@app.get("/health")
def health_check() -> dict:
return {"status": "ok"}
@app.post("/search/lyrics")
def search_lyrics(request: SearchRequest):
try:
return VECTOR_DB.search(request.query, top_k=request.top_k)
except Exception as exc:
raise HTTPException(status_code=500, detail=str(exc)) from exc
@app.post("/search/audio")
def search_audio(request: SearchRequest):
try:
return VECTOR_DB.search(request.query, top_k=request.top_k)
except Exception as exc:
raise HTTPException(status_code=500, detail=str(exc)) from exc
@app.post("/search")
def search(request: SearchRequest):
try:
req_id = uuid4().hex[:8]
start = time.perf_counter()
logger.info(
"[%s] Received fusion search: query='%s', top_k=%s",
req_id,
request.query,
request.top_k,
)
songs = VECTOR_DB.search(request.query, top_k=request.top_k)
logger.info(
"[%s] Search done in %.2fs, matched %s songs. Starting stream...",
req_id,
time.perf_counter() - start,
len(songs),
)
return StreamingResponse(
_song_stream(songs),
media_type="application/x-ndjson",
)
except Exception as exc:
raise HTTPException(status_code=500, detail=str(exc)) from exc
def _song_stream(songs):
total = len(songs)
for idx, song in enumerate(songs, start=1):
logger.info(
"Streaming song %s/%s: %s — %s",
idx,
total,
song["artist"],
song["title"],
)
audio_bytes = Path(song["audio_path"]).read_bytes()
cover_bytes = Path(song["cover_path"]).read_bytes()
payload = {
"title": song["title"],
"artist": song["artist"],
"audio": base64.b64encode(audio_bytes).decode("ascii"),
"cover": base64.b64encode(cover_bytes).decode("ascii"),
}
yield json.dumps(payload).encode("utf-8") + b"\n"
@app.post("/search/combined")
def search_combined(request: SearchRequest):
try:
req_id = uuid4().hex[:8]
start = time.perf_counter()
logger.info(
"[%s] Received combined search: query='%s', top_k=%s",
req_id,
request.query,
request.top_k,
)
songs = VECTOR_DB.search(request.query, top_k=request.top_k)
logger.info(
"[%s] Search done in %.2fs, matched %s songs. Starting stream...",
req_id,
time.perf_counter() - start,
len(songs),
)
return StreamingResponse(
_song_stream(songs),
media_type="application/x-ndjson",
)
except Exception as exc:
raise HTTPException(status_code=500, detail=str(exc)) from exc