Spaces:
Running
Running
Merge pull request #2 from Pushkar222-n/feature/OptimizePrompt
Browse files- config.py +19 -0
- src/api/main.py +5 -2
- src/data_ingestion/load_to_neo4j.py +24 -0
- src/llm/groq_client.py +3 -3
- src/llm/prompts.py +12 -4
- src/retrieval/rag_pipeline.py +2 -1
- src/retrieval/vector_search.py +6 -4
- src/utils/neo4j_client.py +4 -5
- ui/gradio_app.py +491 -70
config.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from pydantic_settings import BaseSettings
|
| 2 |
+
import os
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
class Settings(BaseSettings):
|
| 6 |
+
groq_api_key: str
|
| 7 |
+
neo4j_uri: str
|
| 8 |
+
neo4j_username: str
|
| 9 |
+
neo4j_password: str
|
| 10 |
+
neo4j_database: str
|
| 11 |
+
aura_instanceid: str
|
| 12 |
+
aura_instancename: str
|
| 13 |
+
model_name: str
|
| 14 |
+
|
| 15 |
+
class Config:
|
| 16 |
+
env_file = '.env'
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
settings = Settings() # type: ignore
|
src/api/main.py
CHANGED
|
@@ -4,6 +4,8 @@ from pydantic import BaseModel, Field
|
|
| 4 |
import uvicorn
|
| 5 |
from src.api.schemas import RecommendationRequest, RecommendationResponse
|
| 6 |
import time
|
|
|
|
|
|
|
| 7 |
|
| 8 |
from src.retrieval.rag_pipeline import AnimeRAGPipeline
|
| 9 |
|
|
@@ -78,13 +80,14 @@ async def get_recommendations(request: RecommendationRequest):
|
|
| 78 |
recommendations=result["recommendations"],
|
| 79 |
retrieved_count=result["retrieved_count"],
|
| 80 |
metadata={
|
| 81 |
-
"model":
|
| 82 |
"retriever_k": rag_pipeline.retriever_k,
|
| 83 |
"Time taken for LLM + vector search": str(end_time - start_time)
|
| 84 |
}
|
| 85 |
)
|
| 86 |
|
| 87 |
except Exception as e:
|
|
|
|
| 88 |
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
| 89 |
detail=f"Error processing request: {str(e)}")
|
| 90 |
|
|
@@ -97,7 +100,7 @@ async def get_stats():
|
|
| 97 |
return {
|
| 98 |
"total_anime": rag_pipeline.retriever.collection.count(),
|
| 99 |
"embedding_model": "all-MiniLM-L6-v2",
|
| 100 |
-
"llm_model":
|
| 101 |
"retrieval_k": rag_pipeline.retriever_k
|
| 102 |
}
|
| 103 |
|
|
|
|
| 4 |
import uvicorn
|
| 5 |
from src.api.schemas import RecommendationRequest, RecommendationResponse
|
| 6 |
import time
|
| 7 |
+
from config import settings
|
| 8 |
+
import traceback
|
| 9 |
|
| 10 |
from src.retrieval.rag_pipeline import AnimeRAGPipeline
|
| 11 |
|
|
|
|
| 80 |
recommendations=result["recommendations"],
|
| 81 |
retrieved_count=result["retrieved_count"],
|
| 82 |
metadata={
|
| 83 |
+
"model": settings.model_name,
|
| 84 |
"retriever_k": rag_pipeline.retriever_k,
|
| 85 |
"Time taken for LLM + vector search": str(end_time - start_time)
|
| 86 |
}
|
| 87 |
)
|
| 88 |
|
| 89 |
except Exception as e:
|
| 90 |
+
traceback.print_exc()
|
| 91 |
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
| 92 |
detail=f"Error processing request: {str(e)}")
|
| 93 |
|
|
|
|
| 100 |
return {
|
| 101 |
"total_anime": rag_pipeline.retriever.collection.count(),
|
| 102 |
"embedding_model": "all-MiniLM-L6-v2",
|
| 103 |
+
"llm_model": settings.model_name,
|
| 104 |
"retrieval_k": rag_pipeline.retriever_k
|
| 105 |
}
|
| 106 |
|
src/data_ingestion/load_to_neo4j.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from neo4j import GraphDatabase
|
| 2 |
+
import pandas as pd
|
| 3 |
+
from src.utils.neo4j_client import Neo4j_Client
|
| 4 |
+
|
| 5 |
+
class AnimeGraphLoader:
|
| 6 |
+
"""Loads anime into Neo4j Graph"""
|
| 7 |
+
|
| 8 |
+
def __init__(self):
|
| 9 |
+
self.client = Neo4j_Client()
|
| 10 |
+
|
| 11 |
+
def clear_database(self):
|
| 12 |
+
"""Delete all nodes (use carefully !)"""
|
| 13 |
+
query = "MATCH (n) DETACH DELETE n"
|
| 14 |
+
self.client.run_query(query)
|
| 15 |
+
|
| 16 |
+
def create_constraints(self):
|
| 17 |
+
"""Create uniqueness constraints for performance"""
|
| 18 |
+
queries = [
|
| 19 |
+
"CREATE CONSTRAINT anime_id IF NOT EXISTS FOR (a: Anime) REQUIRE a.mal_id IS UNIQUE",
|
| 20 |
+
"CREATE CONSTRAINT genre_name IF NOT EXISTS FOR (g:Genre) REQUIRE g.name IS UNIQUE",
|
| 21 |
+
"CREATE CONSTRAINT theme_name IF NOT EXISTS FOR (t:Theme) REQUIRE t.name IS UNIQUE",
|
| 22 |
+
"CREATE CONSTRAINT demographic_name IF NOT EXISTS FOR (d:Demographic) REQUIRE d.name IS UNIQUE",
|
| 23 |
+
"CREAE CONSTRAINT content_type IF NOT EXISTS FOR (t: Type) REQUIRE t.type IS UNIUE"
|
| 24 |
+
]
|
src/llm/groq_client.py
CHANGED
|
@@ -2,15 +2,15 @@ import os
|
|
| 2 |
from groq import Groq
|
| 3 |
from dotenv import load_dotenv
|
| 4 |
import logging
|
|
|
|
| 5 |
|
| 6 |
-
load_dotenv()
|
| 7 |
logger = logging.getLogger(__name__)
|
| 8 |
|
| 9 |
|
| 10 |
class GroqLLM:
|
| 11 |
"""Wrapper for Groq API"""
|
| 12 |
|
| 13 |
-
def __init__(self, model: str = "llama-3.
|
| 14 |
"""
|
| 15 |
Initialize Groq Client
|
| 16 |
|
|
@@ -19,7 +19,7 @@ class GroqLLM:
|
|
| 19 |
- llama-3.1-8b-instant: Faster, good enough for most tasks
|
| 20 |
"""
|
| 21 |
|
| 22 |
-
self.client = Groq()
|
| 23 |
self.model = model
|
| 24 |
|
| 25 |
logger.info(f"Initialized Groq with model {self.model}")
|
|
|
|
| 2 |
from groq import Groq
|
| 3 |
from dotenv import load_dotenv
|
| 4 |
import logging
|
| 5 |
+
from config import settings
|
| 6 |
|
|
|
|
| 7 |
logger = logging.getLogger(__name__)
|
| 8 |
|
| 9 |
|
| 10 |
class GroqLLM:
|
| 11 |
"""Wrapper for Groq API"""
|
| 12 |
|
| 13 |
+
def __init__(self, model: str = "llama-3.1-8b-instant"):
|
| 14 |
"""
|
| 15 |
Initialize Groq Client
|
| 16 |
|
|
|
|
| 19 |
- llama-3.1-8b-instant: Faster, good enough for most tasks
|
| 20 |
"""
|
| 21 |
|
| 22 |
+
self.client = Groq(api_key=settings.groq_api_key)
|
| 23 |
self.model = model
|
| 24 |
|
| 25 |
logger.info(f"Initialized Groq with model {self.model}")
|
src/llm/prompts.py
CHANGED
|
@@ -28,16 +28,16 @@ def create_recommendation_prompt(
|
|
| 28 |
context = "\n".join(context_parts)
|
| 29 |
|
| 30 |
prompt = f"""
|
| 31 |
-
You are an expert anime recommendation assistant. A user has asked for recommendations, and
|
| 32 |
First: If the user is asking for a comparison or opinion on specific anime,
|
| 33 |
provide thoughtful comparison rather than a list of recommendations.
|
| 34 |
|
| 35 |
-
If recommendations,
|
| 36 |
Your task is to:
|
| 37 |
1. Analyze the user's request carefully, paying attention to specific preferences (tone, themes, etc.)
|
| 38 |
2. Evaluate each retrieved anime against their user's criteria
|
| 39 |
3. Select the {n_recommendations} BEST matches that truly fit what they're asking for
|
| 40 |
-
4. Explain why each recommendation fits their request
|
| 41 |
|
| 42 |
User's Query:
|
| 43 |
"{user_query}"
|
|
@@ -50,6 +50,8 @@ Instructions:
|
|
| 50 |
- Don't just list all retrieved anime - SELECT the best {n_recommendations} that truly match
|
| 51 |
- For each recommendation, explain in 1-2 sentences WHY it matches their request
|
| 52 |
- If some retrieved anime DON'T match the user's specific criteria, exclude them
|
|
|
|
|
|
|
| 53 |
- Be honest if none of the retrieved anime are great matches
|
| 54 |
|
| 55 |
Format your response as:
|
|
@@ -61,7 +63,13 @@ Format your response as:
|
|
| 61 |
|
| 62 |
[Continue for {n_recommendations} recommendations]
|
| 63 |
|
| 64 |
-
If you think the retrieved anime don't match the request well, say so and explain what type of anime would be better.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 65 |
|
| 66 |
return prompt
|
| 67 |
|
|
|
|
| 28 |
context = "\n".join(context_parts)
|
| 29 |
|
| 30 |
prompt = f"""
|
| 31 |
+
You are an expert anime recommendation assistant. A user has asked for recommendations, and you are provided in context relevant similar anime from the database.
|
| 32 |
First: If the user is asking for a comparison or opinion on specific anime,
|
| 33 |
provide thoughtful comparison rather than a list of recommendations.
|
| 34 |
|
| 35 |
+
## 1. If recommendations, only then:
|
| 36 |
Your task is to:
|
| 37 |
1. Analyze the user's request carefully, paying attention to specific preferences (tone, themes, etc.)
|
| 38 |
2. Evaluate each retrieved anime against their user's criteria
|
| 39 |
3. Select the {n_recommendations} BEST matches that truly fit what they're asking for
|
| 40 |
+
4. Explain why each recommendation fits their request. Answer in a way, that sounds really casual, super relaxed, and don't explain your "system thinking" such as "User has asked for ...., So here they are..." or any such variations.
|
| 41 |
|
| 42 |
User's Query:
|
| 43 |
"{user_query}"
|
|
|
|
| 50 |
- Don't just list all retrieved anime - SELECT the best {n_recommendations} that truly match
|
| 51 |
- For each recommendation, explain in 1-2 sentences WHY it matches their request
|
| 52 |
- If some retrieved anime DON'T match the user's specific criteria, exclude them
|
| 53 |
+
- If some retrieved anime are the sequel (can be identified by name or other information) do not mention them, UNLESS user specifically asks.
|
| 54 |
+
-> If user SPEICIFICALLY Ask about some sequel, and there's no synopsis, tell the User something similar to Synopsis of prequel(that might be in context) and additionally answer based on other featurs like genre, demographics, etc. DO NOT INVENT SYNOPSIS !!!
|
| 55 |
- Be honest if none of the retrieved anime are great matches
|
| 56 |
|
| 57 |
Format your response as:
|
|
|
|
| 63 |
|
| 64 |
[Continue for {n_recommendations} recommendations]
|
| 65 |
|
| 66 |
+
If you think the retrieved anime don't match the request well, say so and explain what type of anime would be better.
|
| 67 |
+
|
| 68 |
+
## 2. If the user wants to talk casually about some anime they recently watched, engage into meaningful conversation with them, and ONLY when it seems user wants recommendations, give recommendation like mentioned in ##1 above.
|
| 69 |
+
## 3. FOR ANY OTHER Gibberish queries, not matching to the "Anime world" except some occasional GREETINGS(in which case you have to greet properly), just give a generic message that "You are not capable of answering that, politely".
|
| 70 |
+
|
| 71 |
+
**Response Style**: Friendly, Casual, and do not mention the programmer(or anything similar to what/how you process/look for data). Just a Intelligent Bot answering to a user.
|
| 72 |
+
"""
|
| 73 |
|
| 74 |
return prompt
|
| 75 |
|
src/retrieval/rag_pipeline.py
CHANGED
|
@@ -2,6 +2,7 @@ from src.retrieval.vector_search import AnimeRetriever
|
|
| 2 |
from src.llm.groq_client import GroqLLM
|
| 3 |
from src.llm.prompts import create_recommendation_prompt, create_system_prompt
|
| 4 |
import logging
|
|
|
|
| 5 |
|
| 6 |
logger = logging.getLogger(__name__)
|
| 7 |
|
|
@@ -24,7 +25,7 @@ class AnimeRAGPipeline:
|
|
| 24 |
recommendation_n: How many to recommend in final output
|
| 25 |
"""
|
| 26 |
self.retriever = retriever or AnimeRetriever()
|
| 27 |
-
self.llm = llm or GroqLLM(model=
|
| 28 |
self.retriever_k = retriever_k
|
| 29 |
self.recommendation_n = recommendation_n
|
| 30 |
|
|
|
|
| 2 |
from src.llm.groq_client import GroqLLM
|
| 3 |
from src.llm.prompts import create_recommendation_prompt, create_system_prompt
|
| 4 |
import logging
|
| 5 |
+
from config import settings
|
| 6 |
|
| 7 |
logger = logging.getLogger(__name__)
|
| 8 |
|
|
|
|
| 25 |
recommendation_n: How many to recommend in final output
|
| 26 |
"""
|
| 27 |
self.retriever = retriever or AnimeRetriever()
|
| 28 |
+
self.llm = llm or GroqLLM(model=settings.model_name)
|
| 29 |
self.retriever_k = retriever_k
|
| 30 |
self.recommendation_n = recommendation_n
|
| 31 |
|
src/retrieval/vector_search.py
CHANGED
|
@@ -52,14 +52,15 @@ class AnimeRetriever:
|
|
| 52 |
distance = results["distances"][0][i] # type: ignore
|
| 53 |
|
| 54 |
# Genre filtering (if specified)
|
| 55 |
-
if genre_filter:
|
| 56 |
-
|
|
|
|
| 57 |
continue
|
| 58 |
|
| 59 |
anime_info = {
|
| 60 |
"mal_id": results["ids"][0][i],
|
| 61 |
"title": metadata["title"],
|
| 62 |
-
"genres": metadata
|
| 63 |
"score": metadata["score"],
|
| 64 |
"type": metadata["type"],
|
| 65 |
"year": metadata["year"],
|
|
@@ -106,4 +107,5 @@ if __name__ == "__main__":
|
|
| 106 |
print("\n=== Test 4: Scored by Filter ===")
|
| 107 |
results = retriever.search("adventure", n_results=5, min_score=8.0)
|
| 108 |
for anime in results:
|
| 109 |
-
print(
|
|
|
|
|
|
| 52 |
distance = results["distances"][0][i] # type: ignore
|
| 53 |
|
| 54 |
# Genre filtering (if specified)
|
| 55 |
+
if genre_filter and "genres" in metadata:
|
| 56 |
+
# type: ignore
|
| 57 |
+
if genre_filter.lower() not in metadata["genres"].lower():
|
| 58 |
continue
|
| 59 |
|
| 60 |
anime_info = {
|
| 61 |
"mal_id": results["ids"][0][i],
|
| 62 |
"title": metadata["title"],
|
| 63 |
+
"genres": metadata.get("genres", ""),
|
| 64 |
"score": metadata["score"],
|
| 65 |
"type": metadata["type"],
|
| 66 |
"year": metadata["year"],
|
|
|
|
| 107 |
print("\n=== Test 4: Scored by Filter ===")
|
| 108 |
results = retriever.search("adventure", n_results=5, min_score=8.0)
|
| 109 |
for anime in results:
|
| 110 |
+
print(
|
| 111 |
+
f"- {anime['title']} (score: {anime['score']}) (scored_by: {anime['scored_by']})")
|
src/utils/neo4j_client.py
CHANGED
|
@@ -1,17 +1,16 @@
|
|
| 1 |
-
import os
|
| 2 |
from dotenv import load_dotenv
|
| 3 |
from neo4j import GraphDatabase
|
| 4 |
|
| 5 |
-
|
| 6 |
|
| 7 |
|
| 8 |
class Neo4j_Client:
|
| 9 |
"""Neo4j database client"""
|
| 10 |
|
| 11 |
def __init__(self):
|
| 12 |
-
uri =
|
| 13 |
-
user =
|
| 14 |
-
password =
|
| 15 |
try:
|
| 16 |
self.driver = GraphDatabase.driver(
|
| 17 |
uri, auth=(user, password)) # type: ignore
|
|
|
|
|
|
|
| 1 |
from dotenv import load_dotenv
|
| 2 |
from neo4j import GraphDatabase
|
| 3 |
|
| 4 |
+
from config import settings
|
| 5 |
|
| 6 |
|
| 7 |
class Neo4j_Client:
|
| 8 |
"""Neo4j database client"""
|
| 9 |
|
| 10 |
def __init__(self):
|
| 11 |
+
uri = settings.neo4j_uri
|
| 12 |
+
user = settings.neo4j_username
|
| 13 |
+
password = settings.neo4j_password
|
| 14 |
try:
|
| 15 |
self.driver = GraphDatabase.driver(
|
| 16 |
uri, auth=(user, password)) # type: ignore
|
ui/gradio_app.py
CHANGED
|
@@ -1,3 +1,117 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
import gradio as gr
|
| 2 |
import requests
|
| 3 |
import os
|
|
@@ -5,98 +119,405 @@ import os
|
|
| 5 |
API_URL = os.getenv("API_URL", "http://127.0.0.1:8000")
|
| 6 |
|
| 7 |
|
| 8 |
-
def get_recommendations(query, min_score, genre_filter):
|
| 9 |
-
"""Call the fastapi backend"""
|
| 10 |
try:
|
| 11 |
-
payload = {
|
| 12 |
-
"query": query,
|
| 13 |
-
"n_results": 5
|
| 14 |
-
}
|
| 15 |
-
|
| 16 |
if min_score > 0:
|
| 17 |
payload["min_score"] = min_score
|
| 18 |
-
|
| 19 |
-
if genre_filter and genre_filter != None:
|
| 20 |
payload["genre_filter"] = genre_filter
|
| 21 |
|
| 22 |
response = requests.post(
|
| 23 |
-
f"{API_URL}/recommend",
|
| 24 |
-
json=payload,
|
| 25 |
-
timeout=30
|
| 26 |
-
)
|
| 27 |
response.raise_for_status()
|
| 28 |
-
|
| 29 |
result = response.json()
|
| 30 |
print(result["metadata"])
|
| 31 |
return result["recommendations"]
|
| 32 |
except requests.exceptions.RequestException as e:
|
| 33 |
-
return f"Error
|
| 34 |
except Exception as e:
|
| 35 |
-
return f"Error: {str(e)}"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 36 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 37 |
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 45 |
""")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 46 |
with gr.Row():
|
| 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 |
# Examples
|
|
|
|
| 77 |
gr.Examples(
|
| 78 |
examples=[
|
| 79 |
-
["
|
| 80 |
-
["Romantic comedy
|
| 81 |
-
["Dark psychological thriller", 8.0, "
|
| 82 |
-
["
|
|
|
|
| 83 |
],
|
| 84 |
-
inputs=[query_input, min_score_slider,
|
|
|
|
|
|
|
| 85 |
)
|
| 86 |
|
| 87 |
-
#
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 93 |
|
| 94 |
-
# Launch
|
| 95 |
if __name__ == "__main__":
|
| 96 |
-
print("Starting
|
| 97 |
-
|
| 98 |
-
demo.launch(
|
| 99 |
-
server_name="0.0.0.0",
|
| 100 |
-
server_port=7860,
|
| 101 |
-
share=True # Set to True to get public URL
|
| 102 |
-
)
|
|
|
|
| 1 |
+
# import gradio as gr
|
| 2 |
+
# import requests
|
| 3 |
+
# import os
|
| 4 |
+
|
| 5 |
+
# API_URL = os.getenv("API_URL", "http://127.0.0.1:8000")
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
# def get_recommendations(query, min_score, genre_filter, n_results):
|
| 9 |
+
# """Call the fastapi backend"""
|
| 10 |
+
# try:
|
| 11 |
+
# payload = {
|
| 12 |
+
# "query": query,
|
| 13 |
+
# "n_results": n_results
|
| 14 |
+
# }
|
| 15 |
+
|
| 16 |
+
# if min_score > 0:
|
| 17 |
+
# payload["min_score"] = min_score
|
| 18 |
+
|
| 19 |
+
# if genre_filter and genre_filter != None:
|
| 20 |
+
# payload["genre_filter"] = genre_filter
|
| 21 |
+
|
| 22 |
+
# response = requests.post(
|
| 23 |
+
# f"{API_URL}/recommend",
|
| 24 |
+
# json=payload,
|
| 25 |
+
# timeout=30
|
| 26 |
+
# )
|
| 27 |
+
# response.raise_for_status()
|
| 28 |
+
|
| 29 |
+
# result = response.json()
|
| 30 |
+
# print(result["metadata"])
|
| 31 |
+
# return result["recommendations"]
|
| 32 |
+
# except requests.exceptions.RequestException as e:
|
| 33 |
+
# return f"Error connecting to the API. Make sure FastAPI server is running. \nDetails: {str(e)}"
|
| 34 |
+
# except Exception as e:
|
| 35 |
+
# return f"Error: {str(e)}"
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
# with gr.Blocks(title="Anime Recommender") as demo:
|
| 39 |
+
# gr.Markdown("""
|
| 40 |
+
# # Anime Recommendation System
|
| 41 |
+
|
| 42 |
+
# Powered by RAG(Retrieval-Augmented Generation)
|
| 43 |
+
|
| 44 |
+
# Ask for anime recommendations and get AI-powered suggestions!
|
| 45 |
+
# """)
|
| 46 |
+
# with gr.Row():
|
| 47 |
+
# with gr.Column(scale=2):
|
| 48 |
+
# query_input = gr.Textbox(
|
| 49 |
+
# label="What are you looking for?",
|
| 50 |
+
# placeholder="e.g., 'Anime similar to Death Note but lighter' or 'Romantic comedy set in high school'",
|
| 51 |
+
# lines=3
|
| 52 |
+
# )
|
| 53 |
+
|
| 54 |
+
# with gr.Row():
|
| 55 |
+
# min_score_slider = gr.Slider(
|
| 56 |
+
# minimum=0,
|
| 57 |
+
# maximum=10,
|
| 58 |
+
# value=0,
|
| 59 |
+
# step=0.5,
|
| 60 |
+
# label="Minimum Rating this animes should have (0 = no filter)"
|
| 61 |
+
# )
|
| 62 |
+
|
| 63 |
+
# genre_dropdown = gr.Dropdown(
|
| 64 |
+
# choices=["None", "Action", "Comedy", "Drama",
|
| 65 |
+
# "Romance", "Sci-Fi", "Fantasy", "Thriller"],
|
| 66 |
+
# value="None",
|
| 67 |
+
# label="Genre Filter (optional)"
|
| 68 |
+
# )
|
| 69 |
+
|
| 70 |
+
# n_results_dropdown = gr.Slider(
|
| 71 |
+
# minimum=1,
|
| 72 |
+
# maximum=8,
|
| 73 |
+
# value=3,
|
| 74 |
+
# step=1,
|
| 75 |
+
# label="Number of recommendation you want to get"
|
| 76 |
+
# )
|
| 77 |
+
|
| 78 |
+
# submit_btn = gr.Button("Get Recommendations",
|
| 79 |
+
# variant="primary", size="lg")
|
| 80 |
+
|
| 81 |
+
# with gr.Column(scale=3):
|
| 82 |
+
# output = gr.Markdown(label="Recommendations")
|
| 83 |
+
|
| 84 |
+
# # Examples
|
| 85 |
+
# gr.Examples(
|
| 86 |
+
# examples=[
|
| 87 |
+
# ["Anime similar to Death Note but lighter", 0, "None", 3],
|
| 88 |
+
# ["Romantic comedy set in high school", 7.5, "Comedy", 4],
|
| 89 |
+
# ["Dark psychological thriller", 8.0, "None", 2],
|
| 90 |
+
# ["Action anime with epic fights", 7.0, "Action", 1],
|
| 91 |
+
# ],
|
| 92 |
+
# inputs=[query_input, min_score_slider,
|
| 93 |
+
# genre_dropdown, n_results_dropdown],
|
| 94 |
+
# )
|
| 95 |
+
|
| 96 |
+
# # Connect button to function
|
| 97 |
+
# submit_btn.click(
|
| 98 |
+
# fn=get_recommendations,
|
| 99 |
+
# inputs=[query_input, min_score_slider,
|
| 100 |
+
# genre_dropdown, n_results_dropdown],
|
| 101 |
+
# outputs=output
|
| 102 |
+
# )
|
| 103 |
+
|
| 104 |
+
# # Launch
|
| 105 |
+
# if __name__ == "__main__":
|
| 106 |
+
# print("Starting Gradio UI...")
|
| 107 |
+
# print("Make sure FastAPI server is running at http://localhost:8000")
|
| 108 |
+
# demo.launch(
|
| 109 |
+
# server_name="0.0.0.0",
|
| 110 |
+
# server_port=7860,
|
| 111 |
+
# share=True # Set to True to get public URL
|
| 112 |
+
# )
|
| 113 |
+
|
| 114 |
+
|
| 115 |
import gradio as gr
|
| 116 |
import requests
|
| 117 |
import os
|
|
|
|
| 119 |
API_URL = os.getenv("API_URL", "http://127.0.0.1:8000")
|
| 120 |
|
| 121 |
|
| 122 |
+
def get_recommendations(query, min_score, genre_filter, n_results):
|
|
|
|
| 123 |
try:
|
| 124 |
+
payload = {"query": query, "n_results": int(n_results)}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 125 |
if min_score > 0:
|
| 126 |
payload["min_score"] = min_score
|
| 127 |
+
if genre_filter and genre_filter != "None":
|
|
|
|
| 128 |
payload["genre_filter"] = genre_filter
|
| 129 |
|
| 130 |
response = requests.post(
|
| 131 |
+
f"{API_URL}/recommend", json=payload, timeout=30)
|
|
|
|
|
|
|
|
|
|
| 132 |
response.raise_for_status()
|
|
|
|
| 133 |
result = response.json()
|
| 134 |
print(result["metadata"])
|
| 135 |
return result["recommendations"]
|
| 136 |
except requests.exceptions.RequestException as e:
|
| 137 |
+
return f"**Connection Error** — Make sure FastAPI is running.\n\n`{str(e)}`"
|
| 138 |
except Exception as e:
|
| 139 |
+
return f"**Error:** {str(e)}"
|
| 140 |
+
|
| 141 |
+
|
| 142 |
+
css = """
|
| 143 |
+
@import url('https://fonts.googleapis.com/css2?family=DM+Sans:wght@300;400;500;600&family=DM+Serif+Display:ital@0;1&display=swap');
|
| 144 |
+
|
| 145 |
+
:root {
|
| 146 |
+
--bg: #1c1714;
|
| 147 |
+
--surface: #241e1a;
|
| 148 |
+
--card: #2d2520;
|
| 149 |
+
--border: #3d3028;
|
| 150 |
+
--accent: #d4845a;
|
| 151 |
+
--gold: #c9a97a;
|
| 152 |
+
--text: #f5ede3;
|
| 153 |
+
--sub: #d0bfae;
|
| 154 |
+
--muted: #9a8878;
|
| 155 |
+
--faint: #6a5a4a;
|
| 156 |
+
--radius: 16px;
|
| 157 |
+
--shadow: 0 4px 28px rgba(0,0,0,0.4);
|
| 158 |
+
}
|
| 159 |
+
|
| 160 |
+
/* ── GLOBAL ── */
|
| 161 |
+
body, .gradio-container {
|
| 162 |
+
background: var(--bg) !important;
|
| 163 |
+
font-family: 'DM Sans', sans-serif !important;
|
| 164 |
+
color: var(--text) !important;
|
| 165 |
+
}
|
| 166 |
+
footer { display: none !important; }
|
| 167 |
+
.gradio-container {
|
| 168 |
+
max-width: 980px !important;
|
| 169 |
+
margin: 0 auto !important;
|
| 170 |
+
padding: 0 36px !important;
|
| 171 |
+
}
|
| 172 |
+
|
| 173 |
+
/* ── HEADER ── */
|
| 174 |
+
.app-header { text-align: center; padding: 56px 0 40px; }
|
| 175 |
+
.app-header h1 {
|
| 176 |
+
font-family: 'DM Serif Display', serif !important;
|
| 177 |
+
font-size: 46px !important;
|
| 178 |
+
font-weight: 400 !important;
|
| 179 |
+
color: var(--text) !important;
|
| 180 |
+
margin: 0 0 8px !important;
|
| 181 |
+
letter-spacing: -0.5px;
|
| 182 |
+
}
|
| 183 |
+
.app-header h1 em { font-style: italic; color: var(--accent); }
|
| 184 |
+
.app-header p { font-size: 15px; color: var(--sub); margin: 0; }
|
| 185 |
+
.header-line { width: 44px; height: 2px; background: var(--accent); border-radius: 2px; margin: 16px auto 0; opacity: 0.75; }
|
| 186 |
|
| 187 |
+
/* ── STEP CARDS ── */
|
| 188 |
+
.step-card {
|
| 189 |
+
background: var(--card);
|
| 190 |
+
border-radius: var(--radius);
|
| 191 |
+
padding: 28px 32px 26px;
|
| 192 |
+
margin-bottom: 14px;
|
| 193 |
+
box-shadow: var(--shadow);
|
| 194 |
+
border: 1px solid var(--border);
|
| 195 |
+
}
|
| 196 |
+
.step-label {
|
| 197 |
+
font-size: 11px; font-weight: 600; letter-spacing: 0.18em;
|
| 198 |
+
text-transform: uppercase; color: var(--muted);
|
| 199 |
+
margin-bottom: 16px; display: flex; align-items: center; gap: 10px;
|
| 200 |
+
}
|
| 201 |
+
.step-label .num {
|
| 202 |
+
width: 21px; height: 21px; background: var(--accent); color: #1c1714;
|
| 203 |
+
border-radius: 50%; display: inline-flex; align-items: center;
|
| 204 |
+
justify-content: center; font-size: 10px; font-weight: 700;
|
| 205 |
+
}
|
| 206 |
|
| 207 |
+
/* ── INPUTS ── */
|
| 208 |
+
textarea, input[type="text"] {
|
| 209 |
+
background: var(--surface) !important;
|
| 210 |
+
border: 1.5px solid var(--border) !important;
|
| 211 |
+
border-radius: 10px !important;
|
| 212 |
+
color: var(--text) !important;
|
| 213 |
+
font-family: 'DM Sans', sans-serif !important;
|
| 214 |
+
font-size: 15px !important;
|
| 215 |
+
transition: border-color 0.2s, box-shadow 0.2s !important;
|
| 216 |
+
}
|
| 217 |
+
textarea::placeholder, input::placeholder { color: var(--faint) !important; }
|
| 218 |
+
textarea:focus, input:focus {
|
| 219 |
+
border-color: var(--accent) !important;
|
| 220 |
+
box-shadow: 0 0 0 3px rgba(212,132,90,0.15) !important;
|
| 221 |
+
outline: none !important;
|
| 222 |
+
}
|
| 223 |
+
label > span {
|
| 224 |
+
font-size: 13px !important; font-weight: 500 !important;
|
| 225 |
+
color: var(--sub) !important; letter-spacing: 0 !important;
|
| 226 |
+
text-transform: none !important;
|
| 227 |
+
}
|
| 228 |
+
|
| 229 |
+
/* ── SLIDER ── */
|
| 230 |
+
input[type="range"] { accent-color: var(--accent) !important; }
|
| 231 |
+
|
| 232 |
+
/* ── SUBMIT BUTTON ── */
|
| 233 |
+
#submit-btn button {
|
| 234 |
+
width: 100% !important;
|
| 235 |
+
background: var(--accent) !important;
|
| 236 |
+
color: #1c1714 !important;
|
| 237 |
+
border: none !important;
|
| 238 |
+
border-radius: 12px !important;
|
| 239 |
+
font-family: 'DM Sans', sans-serif !important;
|
| 240 |
+
font-size: 15px !important;
|
| 241 |
+
font-weight: 700 !important;
|
| 242 |
+
padding: 17px !important;
|
| 243 |
+
cursor: pointer !important;
|
| 244 |
+
letter-spacing: 0.03em !important;
|
| 245 |
+
transition: background 0.2s, transform 0.15s, box-shadow 0.2s !important;
|
| 246 |
+
box-shadow: 0 4px 20px rgba(212,132,90,0.30) !important;
|
| 247 |
+
}
|
| 248 |
+
#submit-btn button:hover {
|
| 249 |
+
background: var(--gold) !important;
|
| 250 |
+
transform: translateY(-2px) !important;
|
| 251 |
+
box-shadow: 0 8px 28px rgba(212,132,90,0.42) !important;
|
| 252 |
+
}
|
| 253 |
+
#submit-btn button:active { transform: translateY(0) !important; }
|
| 254 |
+
|
| 255 |
+
/* ── LOADER ── */
|
| 256 |
+
#ao-loader { display: none; text-align: center; padding: 44px 0 36px; }
|
| 257 |
+
.ao-loader-phrase {
|
| 258 |
+
font-family: 'DM Serif Display', serif;
|
| 259 |
+
font-size: 20px; font-style: italic;
|
| 260 |
+
color: var(--text); margin-bottom: 24px;
|
| 261 |
+
min-height: 30px; transition: opacity 0.35s;
|
| 262 |
+
}
|
| 263 |
+
.ao-dots { display: flex; justify-content: center; gap: 10px; }
|
| 264 |
+
.ao-dots span {
|
| 265 |
+
width: 9px; height: 9px; background: var(--accent);
|
| 266 |
+
border-radius: 50%; animation: aobounce 1.2s infinite ease-in-out;
|
| 267 |
+
}
|
| 268 |
+
.ao-dots span:nth-child(2) { animation-delay: 0.18s; background: var(--gold); }
|
| 269 |
+
.ao-dots span:nth-child(3) { animation-delay: 0.36s; background: var(--faint); }
|
| 270 |
+
@keyframes aobounce {
|
| 271 |
+
0%, 80%, 100% { transform: translateY(0) scale(0.75); opacity: 0.35; }
|
| 272 |
+
40% { transform: translateY(-11px) scale(1.15); opacity: 1; }
|
| 273 |
+
}
|
| 274 |
+
|
| 275 |
+
/* ── OUTPUT BOX ── */
|
| 276 |
+
/* Target the Gradio Markdown block container via elem_id */
|
| 277 |
+
#ao-output-wrap,
|
| 278 |
+
#ao-output-wrap > .wrap,
|
| 279 |
+
div#ao-output-wrap {
|
| 280 |
+
background: var(--card) !important;
|
| 281 |
+
border-radius: var(--radius) !important;
|
| 282 |
+
border: 1px solid var(--border) !important;
|
| 283 |
+
box-shadow: var(--shadow) !important;
|
| 284 |
+
padding: 32px 36px !important;
|
| 285 |
+
margin-top: 4px !important;
|
| 286 |
+
transition: opacity 0.45s !important;
|
| 287 |
+
}
|
| 288 |
+
/* The actual prose inside Markdown */
|
| 289 |
+
#ao-output-wrap .prose,
|
| 290 |
+
#ao-output-wrap .md,
|
| 291 |
+
#ao-output-wrap > div {
|
| 292 |
+
color: var(--sub) !important;
|
| 293 |
+
}
|
| 294 |
+
#ao-output-wrap p,
|
| 295 |
+
#ao-output-wrap .prose p {
|
| 296 |
+
font-size: 14.5px !important;
|
| 297 |
+
line-height: 1.78 !important;
|
| 298 |
+
color: var(--sub) !important;
|
| 299 |
+
margin: 0 0 10px !important;
|
| 300 |
+
}
|
| 301 |
+
#ao-output-wrap h1, #ao-output-wrap h2,
|
| 302 |
+
#ao-output-wrap .prose h1, #ao-output-wrap .prose h2 {
|
| 303 |
+
font-family: 'DM Serif Display', serif !important;
|
| 304 |
+
font-size: 22px !important;
|
| 305 |
+
font-weight: 400 !important;
|
| 306 |
+
color: var(--text) !important;
|
| 307 |
+
padding-bottom: 10px !important;
|
| 308 |
+
border-bottom: 1px solid var(--border) !important;
|
| 309 |
+
margin: 28px 0 12px !important;
|
| 310 |
+
}
|
| 311 |
+
#ao-output-wrap h2:first-child,
|
| 312 |
+
#ao-output-wrap .prose h2:first-child { margin-top: 0 !important; }
|
| 313 |
+
#ao-output-wrap h3,
|
| 314 |
+
#ao-output-wrap .prose h3 {
|
| 315 |
+
font-size: 15px !important;
|
| 316 |
+
color: var(--accent) !important;
|
| 317 |
+
font-weight: 600 !important;
|
| 318 |
+
margin: 0 0 5px !important;
|
| 319 |
+
}
|
| 320 |
+
#ao-output-wrap strong,
|
| 321 |
+
#ao-output-wrap .prose strong { color: var(--text) !important; font-weight: 600 !important; }
|
| 322 |
+
#ao-output-wrap em,
|
| 323 |
+
#ao-output-wrap .prose em { color: var(--gold) !important; }
|
| 324 |
+
#ao-output-wrap code,
|
| 325 |
+
#ao-output-wrap .prose code {
|
| 326 |
+
background: var(--surface) !important;
|
| 327 |
+
color: var(--accent) !important;
|
| 328 |
+
padding: 2px 7px !important;
|
| 329 |
+
border-radius: 5px !important;
|
| 330 |
+
font-size: 13px !important;
|
| 331 |
+
border: 1px solid var(--border) !important;
|
| 332 |
+
}
|
| 333 |
+
#ao-output-wrap ul,
|
| 334 |
+
#ao-output-wrap .prose ul { padding-left: 18px !important; }
|
| 335 |
+
#ao-output-wrap li,
|
| 336 |
+
#ao-output-wrap .prose li {
|
| 337 |
+
font-size: 14.5px !important;
|
| 338 |
+
line-height: 1.72 !important;
|
| 339 |
+
color: var(--sub) !important;
|
| 340 |
+
margin-bottom: 4px !important;
|
| 341 |
+
}
|
| 342 |
+
|
| 343 |
+
/* ── EXAMPLES ── */
|
| 344 |
+
.examples-label {
|
| 345 |
+
font-size: 11px; font-weight: 600; letter-spacing: 0.18em;
|
| 346 |
+
text-transform: uppercase; color: var(--faint); margin: 26px 0 12px;
|
| 347 |
+
}
|
| 348 |
+
.gr-samples-table td, .examples-holder td {
|
| 349 |
+
background: var(--card) !important;
|
| 350 |
+
border: 1px solid var(--border) !important;
|
| 351 |
+
border-radius: 8px !important;
|
| 352 |
+
color: var(--sub) !important;
|
| 353 |
+
font-size: 13px !important; padding: 10px 15px !important;
|
| 354 |
+
transition: all 0.15s !important; cursor: pointer !important;
|
| 355 |
+
font-family: 'DM Sans', sans-serif !important;
|
| 356 |
+
}
|
| 357 |
+
.gr-samples-table tr:hover td, .examples-holder tr:hover td {
|
| 358 |
+
background: var(--accent) !important;
|
| 359 |
+
color: #1c1714 !important;
|
| 360 |
+
border-color: var(--accent) !important;
|
| 361 |
+
}
|
| 362 |
+
.gr-samples-header, .gr-samples-header th { display: none !important; }
|
| 363 |
+
label[data-testid="block-label"] { display: none !important; }
|
| 364 |
+
.examples-holder > label { display: none !important; }
|
| 365 |
+
|
| 366 |
+
/* ── SCROLLBAR ── */
|
| 367 |
+
::-webkit-scrollbar { width: 5px; }
|
| 368 |
+
::-webkit-scrollbar-track { background: var(--bg); }
|
| 369 |
+
::-webkit-scrollbar-thumb { background: var(--border); border-radius: 4px; }
|
| 370 |
+
::-webkit-scrollbar-thumb:hover { background: var(--faint); }
|
| 371 |
+
"""
|
| 372 |
+
|
| 373 |
+
|
| 374 |
+
with gr.Blocks(
|
| 375 |
+
title="Anime Oracle",
|
| 376 |
+
css=css,
|
| 377 |
+
theme=gr.themes.Base(
|
| 378 |
+
primary_hue="orange",
|
| 379 |
+
neutral_hue="slate",
|
| 380 |
+
font=gr.themes.GoogleFont("DM Sans"),
|
| 381 |
+
).set(
|
| 382 |
+
body_background_fill="#1c1714",
|
| 383 |
+
body_text_color="#f5ede3",
|
| 384 |
+
block_background_fill="#2d2520",
|
| 385 |
+
block_border_color="#3d3028",
|
| 386 |
+
input_background_fill="#241e1a",
|
| 387 |
+
input_border_color="#3d3028",
|
| 388 |
+
button_primary_background_fill="#d4845a",
|
| 389 |
+
button_primary_text_color="#1c1714",
|
| 390 |
+
)
|
| 391 |
+
) as demo:
|
| 392 |
+
|
| 393 |
+
gr.HTML("""
|
| 394 |
+
<div class="app-header">
|
| 395 |
+
<h1>Find your next <em>obsession.</em></h1>
|
| 396 |
+
<p>Describe your mood — we'll find the perfect anime.</p>
|
| 397 |
+
<div class="header-line"></div>
|
| 398 |
+
</div>
|
| 399 |
""")
|
| 400 |
+
|
| 401 |
+
# Step 1
|
| 402 |
+
gr.HTML('<div class="step-card"><div class="step-label"><span class="num">1</span> What are you in the mood for?</div>')
|
| 403 |
+
query_input = gr.Textbox(
|
| 404 |
+
label="", show_label=False, lines=3,
|
| 405 |
+
placeholder='"Something like Attack on Titan but more emotional" or "Cozy slice-of-life with great friendships"'
|
| 406 |
+
)
|
| 407 |
+
gr.HTML("</div>")
|
| 408 |
+
|
| 409 |
+
# Step 2
|
| 410 |
+
gr.HTML('<div class="step-card"><div class="step-label"><span class="num">2</span> Any preferences? <span style="font-weight:300;color:#6a5a4a;margin-left:4px;font-size:10px;">optional</span></div>')
|
| 411 |
with gr.Row():
|
| 412 |
+
genre_dropdown = gr.Dropdown(
|
| 413 |
+
choices=["None", "Action", "Comedy", "Drama",
|
| 414 |
+
"Romance", "Sci-Fi", "Fantasy", "Thriller"],
|
| 415 |
+
value="None", label="Genre", scale=1
|
| 416 |
+
)
|
| 417 |
+
min_score_slider = gr.Slider(
|
| 418 |
+
minimum=0, maximum=10, value=0, step=0.5, label="Minimum rating (0 = any)", scale=2)
|
| 419 |
+
gr.HTML("</div>")
|
| 420 |
+
|
| 421 |
+
# Step 3
|
| 422 |
+
gr.HTML('<div class="step-card"><div class="step-label"><span class="num">3</span> How many results?</div>')
|
| 423 |
+
n_results_slider = gr.Slider(
|
| 424 |
+
minimum=1, maximum=8, value=3, step=1, label="", show_label=False)
|
| 425 |
+
gr.HTML("</div>")
|
| 426 |
+
|
| 427 |
+
submit_btn = gr.Button(
|
| 428 |
+
"Find my anime →", variant="primary", elem_id="submit-btn")
|
| 429 |
+
|
| 430 |
+
# Loading buffer
|
| 431 |
+
gr.HTML("""
|
| 432 |
+
<div id="ao-loader">
|
| 433 |
+
<div class="ao-loader-phrase">Scanning the anime universe…</div>
|
| 434 |
+
<div class="ao-dots"><span></span><span></span><span></span></div>
|
| 435 |
+
</div>
|
| 436 |
+
""")
|
| 437 |
+
|
| 438 |
+
# Output — elem_id so CSS can target it reliably
|
| 439 |
+
output = gr.Markdown(
|
| 440 |
+
value="*Your recommendations will appear here ✦*",
|
| 441 |
+
elem_id="ao-output-wrap"
|
| 442 |
+
)
|
| 443 |
|
| 444 |
# Examples
|
| 445 |
+
gr.HTML('<div class="examples-label">Try an example</div>')
|
| 446 |
gr.Examples(
|
| 447 |
examples=[
|
| 448 |
+
["Something like Death Note but with a lighter tone", 0, "None", 3],
|
| 449 |
+
["Romantic comedy with lovable characters", 7.5, "Comedy", 4],
|
| 450 |
+
["Dark psychological thriller with twists", 8.0, "Thriller", 2],
|
| 451 |
+
["Epic action with incredible fights", 7.0, "Action", 5],
|
| 452 |
+
["Wholesome feel-good slice of life", 6.0, "Drama", 3],
|
| 453 |
],
|
| 454 |
+
inputs=[query_input, min_score_slider,
|
| 455 |
+
genre_dropdown, n_results_slider],
|
| 456 |
+
label=""
|
| 457 |
)
|
| 458 |
|
| 459 |
+
# Loader JS
|
| 460 |
+
gr.HTML("""
|
| 461 |
+
<script>
|
| 462 |
+
(function() {
|
| 463 |
+
const PHRASES = [
|
| 464 |
+
"Scanning the anime universe\u2026",
|
| 465 |
+
"Consulting the oracle\u2026",
|
| 466 |
+
"Traversing story arcs\u2026",
|
| 467 |
+
"Matching your vibe\u2026",
|
| 468 |
+
"Sifting through thousands of titles\u2026",
|
| 469 |
+
"Almost there\u2026"
|
| 470 |
+
];
|
| 471 |
+
let phraseTimer = null, phraseIdx = 0;
|
| 472 |
+
|
| 473 |
+
function startLoader() {
|
| 474 |
+
const loader = document.getElementById('ao-loader');
|
| 475 |
+
const wrap = document.getElementById('ao-output-wrap');
|
| 476 |
+
if (loader) loader.style.display = 'block';
|
| 477 |
+
if (wrap) wrap.style.opacity = '0.15';
|
| 478 |
+
phraseIdx = 0;
|
| 479 |
+
const el = document.querySelector('.ao-loader-phrase');
|
| 480 |
+
if (el) el.textContent = PHRASES[0];
|
| 481 |
+
clearInterval(phraseTimer);
|
| 482 |
+
phraseTimer = setInterval(() => {
|
| 483 |
+
phraseIdx = (phraseIdx + 1) % PHRASES.length;
|
| 484 |
+
if (el) {
|
| 485 |
+
el.style.opacity = '0';
|
| 486 |
+
setTimeout(() => { el.textContent = PHRASES[phraseIdx]; el.style.opacity = '1'; }, 300);
|
| 487 |
+
}
|
| 488 |
+
}, 1800);
|
| 489 |
+
}
|
| 490 |
+
|
| 491 |
+
function stopLoader() {
|
| 492 |
+
clearInterval(phraseTimer);
|
| 493 |
+
const loader = document.getElementById('ao-loader');
|
| 494 |
+
const wrap = document.getElementById('ao-output-wrap');
|
| 495 |
+
if (loader) loader.style.display = 'none';
|
| 496 |
+
if (wrap) { wrap.style.transition = 'opacity 0.5s ease'; wrap.style.opacity = '1'; }
|
| 497 |
+
}
|
| 498 |
+
|
| 499 |
+
document.addEventListener('click', function(e) {
|
| 500 |
+
if (e.target && e.target.closest('#submit-btn')) {
|
| 501 |
+
startLoader();
|
| 502 |
+
setTimeout(stopLoader, 25000);
|
| 503 |
+
}
|
| 504 |
+
});
|
| 505 |
+
|
| 506 |
+
function watchOutput() {
|
| 507 |
+
const target = document.getElementById('ao-output-wrap');
|
| 508 |
+
if (!target) { setTimeout(watchOutput, 500); return; }
|
| 509 |
+
new MutationObserver(stopLoader).observe(target, { childList: true, subtree: true, characterData: true });
|
| 510 |
+
}
|
| 511 |
+
watchOutput();
|
| 512 |
+
})();
|
| 513 |
+
</script>
|
| 514 |
+
""")
|
| 515 |
+
|
| 516 |
+
submit_btn.click(fn=get_recommendations, inputs=[
|
| 517 |
+
query_input, min_score_slider, genre_dropdown, n_results_slider], outputs=output)
|
| 518 |
+
query_input.submit(fn=get_recommendations, inputs=[
|
| 519 |
+
query_input, min_score_slider, genre_dropdown, n_results_slider], outputs=output)
|
| 520 |
|
|
|
|
| 521 |
if __name__ == "__main__":
|
| 522 |
+
print("Starting Anime Oracle...")
|
| 523 |
+
demo.launch(server_name="0.0.0.0", server_port=7860, share=True)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|