import os import pandas as pd import numpy as np import gradio as gr from langchain_community.document_loaders import TextLoader from langchain_text_splitters import CharacterTextSplitter from langchain_chroma import Chroma from langchain_huggingface import HuggingFaceEmbeddings # ========================================================= # BASE DIRECTORY — works both locally and on HuggingFace # ========================================================= BASE_DIR = os.path.dirname(os.path.abspath(__file__)) # ========================================================= # EMBEDDING MODEL # ========================================================= embedding_model = HuggingFaceEmbeddings( model_name="sentence-transformers/all-MiniLM-L6-v2" ) # ========================================================= # LOAD BOOK DATASET # ========================================================= books = pd.read_csv(os.path.join(BASE_DIR, "books_with_emotions.csv")) # ========================================================= # CLEAN CATEGORY COLUMN # ========================================================= books["simple_categories"] = ( books["simple_categories"] .fillna("") .astype(str) .str.strip() ) # ========================================================= # HANDLE THUMBNAILS # ========================================================= books["thumbnail"] = books["thumbnail"].fillna("") books["large_thumbnail"] = np.where( books["thumbnail"] != "", books["thumbnail"] + "&fife=w800", "cover-not-found.png" ) # ========================================================= # LOAD EXISTING VECTOR DATABASE # NOTE: ChromaDB files must be inside a "chroma_db" folder # at the root of your HuggingFace Space repo. # Required files inside chroma_db/: # - chroma.sqlite3 # - data_level0.bin # - header.bin # - length.bin # - link_lists.bin # - index_metadata.pickle # ========================================================= CHROMA_PATH = os.path.join(BASE_DIR, "chroma_db") db_books = Chroma( persist_directory=CHROMA_PATH, embedding_function=embedding_model ) # ========================================================= # RETRIEVE SEMANTIC RECOMMENDATIONS # ========================================================= def retrieve_semantic_recommendations( query: str, category: str, tone: str = "All", initial_top_k: int = 50, final_top_k: int = 16 ) -> pd.DataFrame: recs = db_books.similarity_search(query, k=initial_top_k) books_list = [] for rec in recs: try: isbn = int(rec.page_content.strip('"').split()[0]) books_list.append(isbn) except Exception: continue # Preserve vector search order and deduplicate seen = set() valid_isbns = [] for isbn in books_list: if isbn in books["isbn13"].values and isbn not in seen: valid_isbns.append(isbn) seen.add(isbn) if len(valid_isbns) == 0: return pd.DataFrame() book_recs = ( books .set_index("isbn13") .loc[valid_isbns] .reset_index() ) # Category filter if category != "All": book_recs = book_recs[ book_recs["simple_categories"].str.lower() == category.strip().lower() ] # Emotional tone filter tone_map = { "Happy": "joy", "Surprising": "surprise", "Angry": "anger", "Suspenseful": "fear", "Sad": "sadness", } if tone in tone_map: book_recs = book_recs.sort_values( by=tone_map[tone], ascending=False ) return book_recs.head(final_top_k) # ========================================================= # GENERATE HTML BOOK CARDS # ========================================================= def recommend_books(query: str, category: str, tone: str): if not query.strip(): return """
💡 Please enter a book description to get started.
""" recommendations = retrieve_semantic_recommendations( query=query, category=category, tone=tone ) if recommendations.empty: return """
😔 No books found for this combination.
""" html = """
""" for _, row in recommendations.iterrows(): title = str(row["title"]) authors = str(row["authors"]) description = str(row["description"]) short_description = " ".join(description.split()[:35]) + "..." authors_split = authors.split(";") if len(authors_split) == 2: authors_str = f"{authors_split[0]} and {authors_split[1]}" elif len(authors_split) > 2: authors_str = ( f"{', '.join(authors_split[:-1])} and {authors_split[-1]}" ) else: authors_str = authors google_books_link = ( f"https://books.google.com/books?vid=ISBN{row['isbn13']}" ) amazon_link = ( f"https://www.amazon.com/s?k={title.replace(' ', '+')}" ) html += f"""

{title}

by {authors_str}

{short_description}

📖 Read Preview 🛒 Buy Book
""" html += "
" return html # ========================================================= # DROPDOWNS # ========================================================= categories = ["All"] + sorted( books["simple_categories"] .dropna() .str.strip() .unique() .tolist() ) tones = ["All", "Happy", "Surprising", "Angry", "Suspenseful", "Sad"] # ========================================================= # GRADIO DASHBOARD # ========================================================= with gr.Blocks( theme=gr.themes.Soft(), css="footer { visibility: hidden; }" ) as dashboard: gr.Markdown( """ # 📚 Semantic Book Recommender Discover books using AI-powered semantic search, emotional tones, and category filtering. """ ) with gr.Row(): user_query = gr.Textbox( label="Enter Book Description", placeholder="e.g., A dark fantasy story with dragons and magic...", scale=3 ) category_dropdown = gr.Dropdown( choices=categories, label="Select Category", value="All", scale=1 ) tone_dropdown = gr.Dropdown( choices=tones, label="Select Emotional Tone", value="All", scale=1 ) submit_button = gr.Button("🔍 Find Recommendations", variant="primary") output = gr.HTML() submit_button.click( fn=recommend_books, inputs=[user_query, category_dropdown, tone_dropdown], outputs=output ) # ========================================================= # RUN APP # ========================================================= if __name__ == "__main__": dashboard.launch(server_name="0.0.0.0", server_port=7860)