| from fastapi import FastAPI, HTTPException |
| from pydantic import BaseModel |
| from sentence_transformers import SentenceTransformer |
| import torch |
| import pandas as pd |
| import numpy as np |
| import os |
| import gdown |
|
|
| |
| |
| |
| MODEL_DRIVE_ID = "1qdW9UCJlZSSlSRahx30UrLtZ_3MRMm9e" |
| CSV_DRIVE_ID = "1lzJU7hjG75uQ4hL02Gzy_vU1FtiHOImT" |
|
|
| if not os.path.exists("trained_model"): |
| print("Downloading model from Google Drive...") |
| os.makedirs("trained_model", exist_ok=True) |
| gdown.download_folder(id=MODEL_DRIVE_ID, output="trained_model", quiet=False) |
| print("✓ Model downloaded") |
|
|
| if not os.path.exists("projects.csv"): |
| print("Downloading projects.csv from Google Drive...") |
| gdown.download(id=CSV_DRIVE_ID, output="projects.csv", quiet=False) |
| print("✓ projects.csv downloaded") |
|
|
| |
| |
| |
| app = FastAPI( |
| title="Project Idea Similarity API", |
| description="Semantic similarity search for graduation projects", |
| version="1.0" |
| ) |
|
|
| def clean_text(text: str) -> str: |
| text = str(text).strip() |
| text = " ".join(text.split()) |
| return text |
|
|
| print("Loading model...") |
| model = SentenceTransformer("trained_model") |
| print("✓ Model loaded") |
|
|
| print("Loading dataset...") |
| df = pd.read_csv("projects.csv", encoding="latin1") |
| df.rename(columns={'Name': 'Name'}, inplace=True) |
| df["project_id"] = df.index + 1 |
| required_cols = {"Name", "Abstract", "Date"} |
| print("Number of records:", len(df)) |
| if not required_cols.issubset(df.columns): |
| raise RuntimeError("Dataset must contain Name, Abstract, Date columns") |
|
|
| df["Abstract"] = df["Abstract"].apply(clean_text) |
| projects = df.to_dict(orient="records") |
|
|
| EMBEDDINGS_PATH = "/tmp/embeddings.npy" |
| try: |
| print("Loading embeddings...") |
| embeddings = np.load(EMBEDDINGS_PATH) |
| embeddings = torch.tensor(embeddings) |
| print("✓ Embeddings loaded") |
| except FileNotFoundError: |
| print("Computing embeddings...") |
| embeddings = model.encode( |
| df["Abstract"].tolist(), |
| convert_to_tensor=True, |
| show_progress_bar=True |
| ) |
| np.save(EMBEDDINGS_PATH, embeddings.cpu().numpy()) |
| print("✓ Embeddings computed & saved") |
|
|
| |
| |
| |
| class SimilarityRequest(BaseModel): |
| abstract: str |
| top_k: int = 5 |
|
|
| class SimilarProject(BaseModel): |
| project_id: int |
| project_name: str |
| project_abstract: str |
| project_date: str |
| similarity_score: float |
|
|
| class SimilarityResponse(BaseModel): |
| input_abstract: str |
| top_k: int |
| results: list[SimilarProject] |
|
|
| |
| |
| |
| @app.get("/api/health") |
| def health(): |
| return { |
| "status": "ok", |
| "model_loaded": True, |
| "projects_loaded": len(projects) |
| } |
|
|
| @app.post("/api/similarity", response_model=SimilarityResponse) |
| def find_similarity(request: SimilarityRequest): |
| if not request.abstract or len(request.abstract.strip()) < 20: |
| raise HTTPException( |
| status_code=400, |
| detail="Project abstract must be at least 20 characters" |
| ) |
| cleaned_input = clean_text(request.abstract) |
| input_embedding = model.encode(cleaned_input, convert_to_tensor=True) |
| scores = torch.nn.functional.cosine_similarity( |
| input_embedding.unsqueeze(0), |
| embeddings |
| ) |
| top_k = min(request.top_k, len(projects)) |
| top_scores, top_indices = torch.topk(scores, k=top_k) |
| results = [] |
| for score, idx in zip(top_scores, top_indices): |
| project = projects[idx.item()] |
| results.append({ |
| "project_id": project["project_id"]+1, |
| "project_name": project["Name"], |
| "project_abstract": project["Abstract"], |
| "project_date": str(project["Date"]), |
| "similarity_score": round(score.item(), 4) |
| }) |
| return { |
| "input_abstract": cleaned_input, |
| "top_k": top_k, |
| "results": results |
| } |