Download backend/main.py from smlflg/BookmarkBrain: direct link, hf CLI and curl.
- Browser
- Download file 6.99 kB
-
https://huggingface.co/smlflg/BookmarkBrain/resolve/main/backend/main.py
- Command line
-
hf download hf://smlflg/BookmarkBrain/backend/main.py
-
curl -L -o main.py https://huggingface.co/smlflg/BookmarkBrain/resolve/main/backend/main.py
6.99 kB
| import json | |
| from pathlib import Path | |
| from fastapi import FastAPI, Request, UploadFile, File, Query, HTTPException | |
| from fastapi.middleware.cors import CORSMiddleware | |
| from fastapi.responses import HTMLResponse, JSONResponse | |
| from fastapi.templating import Jinja2Templates | |
| from backend.database import get_db, init_db, add_bookmark, get_bookmark, update_bookmark, delete_bookmark, list_bookmarks, get_all_tags, get_stats | |
| from backend.search import hybrid_search | |
| from backend.models import BookmarkCreate, BookmarkUpdate | |
| from backend.embeddings import encode, build_embedding_text | |
| from backend.tagger import generate_tags | |
| from backend.importer import import_bookmarks | |
| app = FastAPI(title="BookmarkBrain", version="0.1.0") | |
| app.add_middleware( | |
| CORSMiddleware, | |
| allow_origins=["*"], # chrome-extension://* and moz-extension://* | |
| allow_credentials=True, | |
| allow_methods=["*"], | |
| allow_headers=["*"], | |
| ) | |
| TEMPLATES_DIR = Path(__file__).parent / "templates" | |
| templates = Jinja2Templates(directory=str(TEMPLATES_DIR)) | |
| templates.env.filters["from_json"] = json.loads | |
| def startup(): | |
| init_db() | |
| # --- Web UI --- | |
| async def index(request: Request): | |
| with get_db() as conn: | |
| tags = get_all_tags(conn) | |
| stats = get_stats(conn) | |
| return templates.TemplateResponse("index.html", { | |
| "request": request, | |
| "tags": tags, | |
| "stats": stats, | |
| }) | |
| # --- Search --- | |
| async def search(request: Request, q: str = Query(..., min_length=1), limit: int = Query(10, ge=1, le=50)): | |
| results = hybrid_search(q, limit=limit) | |
| # If htmx request, return HTML partial | |
| if "HX-Request" in request.headers: | |
| return templates.TemplateResponse("components/search_results.html", { | |
| "request": request, | |
| "results": results, | |
| "query": q, | |
| }) | |
| return {"results": results, "query": q, "total": len(results)} | |
| # --- CRUD --- | |
| async def api_list_bookmarks(page: int = Query(1, ge=1), per_page: int = Query(20, ge=1, le=100)): | |
| with get_db() as conn: | |
| bookmarks, total = list_bookmarks(conn, page=page, per_page=per_page) | |
| return {"bookmarks": bookmarks, "total": total, "page": page, "per_page": per_page} | |
| async def api_get_bookmark(bookmark_id: int): | |
| with get_db() as conn: | |
| bm = get_bookmark(conn, bookmark_id) | |
| if not bm: | |
| raise HTTPException(status_code=404, detail="Bookmark not found") | |
| return bm | |
| async def api_create_bookmark(data: BookmarkCreate): | |
| from urllib.parse import urlparse | |
| domain = urlparse(data.url).netloc | |
| auto_tags = generate_tags(data.url, data.title, domain=domain) | |
| embedding_text = build_embedding_text(data.title, data.description, data.context, auto_tags, domain) | |
| embedding_blob = encode(embedding_text) | |
| with get_db() as conn: | |
| bookmark_id = add_bookmark( | |
| conn, url=data.url, title=data.title, description=data.description, | |
| context=data.context, folder_path="", domain=domain, | |
| auto_tags=auto_tags, user_tags=data.user_tags or [], | |
| content_snippet="", embedding_text=embedding_text, embedding_blob=embedding_blob, | |
| ) | |
| conn.commit() | |
| bm = get_bookmark(conn, bookmark_id) | |
| return bm | |
| async def api_update_bookmark(bookmark_id: int, data: BookmarkUpdate): | |
| with get_db() as conn: | |
| existing = get_bookmark(conn, bookmark_id) | |
| if not existing: | |
| raise HTTPException(status_code=404, detail="Bookmark not found") | |
| updates = data.model_dump(exclude_none=True) | |
| if "user_tags" in updates: | |
| updates["user_tags"] = json.dumps(updates["user_tags"]) | |
| update_bookmark(conn, bookmark_id, **updates) | |
| # Re-embed if content changed | |
| if any(k in updates for k in ("title", "description", "context")): | |
| bm = get_bookmark(conn, bookmark_id) | |
| auto_tags = json.loads(bm["auto_tags"]) if isinstance(bm["auto_tags"], str) else bm["auto_tags"] | |
| embedding_text = build_embedding_text(bm["title"], bm["description"], bm["context"], auto_tags, bm["domain"]) | |
| embedding_blob = encode(embedding_text) | |
| # Update vec_bookmarks | |
| conn.execute("DELETE FROM vec_bookmarks WHERE id = ?", (bookmark_id,)) | |
| conn.execute("INSERT INTO vec_bookmarks (id, embedding) VALUES (?, ?)", (bookmark_id, embedding_blob)) | |
| conn.execute("UPDATE bookmarks SET embedding_text = ? WHERE id = ?", (embedding_text, bookmark_id)) | |
| conn.commit() | |
| return get_bookmark(conn, bookmark_id) | |
| async def api_delete_bookmark(bookmark_id: int): | |
| with get_db() as conn: | |
| existing = get_bookmark(conn, bookmark_id) | |
| if not existing: | |
| raise HTTPException(status_code=404, detail="Bookmark not found") | |
| delete_bookmark(conn, bookmark_id) | |
| conn.commit() | |
| return {"status": "deleted", "id": bookmark_id} | |
| # --- Import --- | |
| async def api_import(file: UploadFile = File(...)): | |
| import tempfile | |
| suffix = ".html" if file.filename and file.filename.endswith(".html") else ".html" | |
| with tempfile.NamedTemporaryFile(suffix=suffix, delete=False, mode="wb") as tmp: | |
| content = await file.read() | |
| tmp.write(content) | |
| tmp_path = tmp.name | |
| result = import_bookmarks(tmp_path) | |
| Path(tmp_path).unlink(missing_ok=True) | |
| return result | |
| # --- Tags & Stats --- | |
| async def api_tags(): | |
| with get_db() as conn: | |
| tags = get_all_tags(conn) | |
| return [{"tag": t, "count": c} for t, c in tags] | |
| async def api_stats(): | |
| with get_db() as conn: | |
| return get_stats(conn) | |
| # --- Reindex --- | |
| async def api_reindex(): | |
| from backend.embeddings import encode_batch, build_embedding_text | |
| with get_db() as conn: | |
| rows = conn.execute("SELECT id, title, description, context, auto_tags, domain FROM bookmarks").fetchall() | |
| texts = [] | |
| ids = [] | |
| for row in rows: | |
| auto_tags = json.loads(row["auto_tags"]) if isinstance(row["auto_tags"], str) else row["auto_tags"] | |
| text = build_embedding_text(row["title"], row["description"], row["context"], auto_tags, row["domain"]) | |
| texts.append(text) | |
| ids.append(row["id"]) | |
| blobs = encode_batch(texts) | |
| # Clear and rebuild vec table | |
| conn.execute("DELETE FROM vec_bookmarks") | |
| for id, blob, text in zip(ids, blobs, texts): | |
| conn.execute("INSERT INTO vec_bookmarks (id, embedding) VALUES (?, ?)", (id, blob)) | |
| conn.execute("UPDATE bookmarks SET embedding_text = ? WHERE id = ?", (text, id)) | |
| conn.commit() | |
| return {"reindexed": len(ids)} | |