ShunTay12 commited on
Commit
cfcf570
·
0 Parent(s):

Delete model files to prevent Binary files

Browse files
.gitignore ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Python-generated files
2
+ __pycache__/
3
+ *.py[oc]
4
+ build/
5
+ dist/
6
+ wheels/
7
+ *.egg-info
8
+
9
+ # Virtual environments
10
+ .venv
11
+ .env
.python-version ADDED
@@ -0,0 +1 @@
 
 
1
+ 3.13
Dockerfile ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # syntax=docker/dockerfile:1.7-labs
2
+ FROM ghcr.io/astral-sh/uv:python3.13-bookworm
3
+
4
+ # App directory
5
+ WORKDIR /app
6
+
7
+ # Install dependencies with uv (uses cache for speed)
8
+ COPY pyproject.toml ./
9
+ RUN --mount=type=cache,target=/root/.cache/uv \
10
+ uv sync --no-dev --python /usr/local/bin/python3
11
+
12
+ # Copy the rest of the project
13
+ COPY . .
14
+
15
+ # Ensure uv uses the project virtualenv
16
+ ENV UV_PROJECT_ENVIRONMENT=/app/.venv
17
+
18
+ EXPOSE 7860
19
+
20
+ # Start the FastAPI app
21
+ CMD ["uv", "run", "main.py"]
README.md ADDED
@@ -0,0 +1,75 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Model FastAPI
2
+
3
+ FastAPI service that provides two capabilities:
4
+
5
+ - Deepfake image detection using a SigLIP image-classification backbone with a LoRA adapter.
6
+ - A news-aware chatbot that selectively performs web search via Tavily and responds with verified sources when evidence is found.
7
+
8
+ ## Project Layout
9
+
10
+ - main app entry: main.py
11
+ - Deepfake detector: app/detector.py, app/core/detector/_, app/services/detector/_, model/output/siglip-lora-optimized/
12
+ - Chatbot: app/chatbot.py, app/core/chatbot/_, app/services/chatbot/_, app/schemas/chat.py
13
+ - Database migration (chat history table): supabase/migrations/
14
+
15
+ ## Requirements
16
+
17
+ - Python 3.13+
18
+ - uv package manager (https://docs.astral.sh/uv/)
19
+ - Access to the SigLIP base model and the LoRA adapter stored at model/output/siglip-lora-optimized/
20
+
21
+ ## Environment Variables
22
+
23
+ - DATABASE_URL (or SUPABASE_DATABASE_URL): PostgreSQL connection string for chat history (if save_to_db=true).
24
+ - GROQ_API_KEY: required by the Groq LLM used for responses.
25
+ - OPENROUTER_API_KEY: required by the OpenRouter model used for classification/query rewriting.
26
+ - TAVILY_API_KEY: required for Tavily search.
27
+ - Optional: set CUDA-visible devices as needed for GPU inference.
28
+
29
+ ## Setup (local)
30
+
31
+ ```bash
32
+ uv sync
33
+ uv run main.py # starts FastAPI on 0.0.0.0:7860
34
+ ```
35
+
36
+ The detector will load the SigLIP base model and apply the LoRA adapter from model/output/siglip-lora-optimized/.
37
+
38
+ ## API
39
+
40
+ - POST /detect
41
+ - Form-data file field: file (image). Returns predicted_class (index), predicted_label (from id2label), prediction (real/fake thresholded at P(real) >= 0.90), confidence, and class probabilities.
42
+ - POST /chat
43
+ - JSON: {"query": "...", "session_id": "optional", "save_to_db": true|false}
44
+ - Auto-classifies need for search, optionally queries Tavily, then responds. Returns response.content, session_id, used_search, and search_reason.
45
+ - DELETE /chat/{session_id}
46
+ - Clears chat history (both in-memory guest sessions and DB rows).
47
+
48
+ ### Quick cURL examples
49
+
50
+ ```bash
51
+ # Deepfake detection
52
+ curl -X POST "http://localhost:7860/detect" \
53
+ -F "file=@path/to/image.jpg"
54
+
55
+ # Chat (without forcing search decision)
56
+ curl -X POST "http://localhost:7860/chat" \
57
+ -H "Content-Type: application/json" \
58
+ -d '{"query": "Is the latest SpaceX launch successful?", "save_to_db": false}'
59
+ ```
60
+
61
+ ## Docker
62
+
63
+ A Dockerfile is provided using the uv base image.
64
+
65
+ ```bash
66
+ docker build -t model-fast-api .
67
+ docker run -p 7860:7860 --env-file .env model-fast-api
68
+ ```
69
+
70
+ The image installs dependencies with uv sync and runs `uv run main.py`.
71
+
72
+ ## Notes
73
+
74
+ - The news verification prompt only returns source links when the claim is supported by the retrieved results; otherwise it replies with UNDETERMINED and no links.
75
+ - If chat history persistence is disabled (save_to_db=false), sessions are stored in-memory.
app/chatbot.py ADDED
@@ -0,0 +1,138 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Chatbot API routes.
3
+ """
4
+
5
+ import logging
6
+ from uuid import uuid4
7
+
8
+ from fastapi import APIRouter, Depends, HTTPException
9
+ from psycopg import Connection
10
+ from langchain_core.runnables.history import RunnableWithMessageHistory
11
+ from langchain_tavily import TavilySearch
12
+
13
+ from app.core.chatbot.config import CHAT_HISTORY_TABLE
14
+ from app.core.chatbot.chains import general_chain, news_chain
15
+ from app.core.chatbot.database import db_connection_dependency
16
+ from app.schemas.chat import ChatRequest
17
+ from app.services.chatbot.history import delete_guest_session, get_message_history
18
+ from app.services.chatbot.search import (
19
+ extract_search_query,
20
+ format_search_results,
21
+ get_tavily_search,
22
+ should_use_search,
23
+ )
24
+
25
+ logger = logging.getLogger(__name__)
26
+
27
+ chat = APIRouter()
28
+
29
+
30
+ @chat.post("/chat")
31
+ async def chat_endpoint(
32
+ request: ChatRequest,
33
+ tavily_search: TavilySearch = Depends(get_tavily_search),
34
+ ):
35
+ """
36
+ Chat endpoint with intelligent query processing.
37
+
38
+ Flow:
39
+ 1. LLM classifies if query needs web search
40
+ 2. If NO search needed -> respond directly with LLM knowledge
41
+ 3. If search needed -> extract optimized search query -> search with Tavily -> respond with sources
42
+
43
+ Returns:
44
+ response: The LLM's response
45
+ session_id: Session ID for conversation history
46
+ used_search: Whether web search was used
47
+ search_reason: Why search was/wasn't used
48
+ """
49
+ try:
50
+ # Ensure every session has an id for history tracking (DB or in-memory)
51
+ session_id = request.session_id or str(uuid4())
52
+ save_to_db = request.save_to_db
53
+
54
+ # Step 1: LLM decides if search is needed
55
+ needs_search, search_reason = should_use_search(request.query)
56
+ logger.info("Search needed: %s, Reason: %s", needs_search, search_reason)
57
+
58
+ def get_history(sid: str):
59
+ return get_message_history(sid, save_to_db)
60
+
61
+ # Step 2: Take appropriate path
62
+ if not needs_search:
63
+ # Respond directly without search
64
+ chain_with_history = RunnableWithMessageHistory(
65
+ general_chain,
66
+ get_history,
67
+ input_messages_key="question",
68
+ history_messages_key="chat_history",
69
+ )
70
+ response = chain_with_history.invoke(
71
+ {"question": request.query},
72
+ config={"configurable": {"session_id": session_id}},
73
+ )
74
+ else:
75
+ # Step 2a: Extract optimized search query
76
+ optimized_query = extract_search_query(request.query)
77
+ logger.info("Optimized search query: %s", optimized_query)
78
+ # Step 2b: Search with optimized query
79
+ search_results = tavily_search.invoke(optimized_query)
80
+ formatted_results = format_search_results(search_results)
81
+ logger.debug("Search results: %s", search_results)
82
+
83
+ # Step 2c: Respond with sources
84
+ chain_with_history = RunnableWithMessageHistory(
85
+ news_chain,
86
+ get_history,
87
+ input_messages_key="question",
88
+ history_messages_key="chat_history",
89
+ )
90
+ response = chain_with_history.invoke(
91
+ {
92
+ "question": request.query,
93
+ "search_results": formatted_results,
94
+ },
95
+ config={"configurable": {"session_id": session_id}},
96
+ )
97
+
98
+ logger.debug("Response: %s", response.content)
99
+
100
+ return {
101
+ "response": {
102
+ "content": response.content,
103
+ },
104
+ "session_id": session_id,
105
+ "used_search": needs_search,
106
+ "search_reason": search_reason,
107
+ }
108
+ except HTTPException:
109
+ raise
110
+ except Exception as exc: # pragma: no cover - defensive server guard
111
+ logger.exception("Unhandled error in chat_endpoint")
112
+ raise HTTPException(status_code=500, detail=str(exc))
113
+
114
+
115
+ @chat.delete("/chat/{session_id}")
116
+ async def delete_chat_history(session_id: str, conn: Connection = Depends(db_connection_dependency)):
117
+ """Delete chat history for a specific session."""
118
+ try:
119
+ # Delete from in-memory guest sessions
120
+ delete_guest_session(session_id)
121
+
122
+ # Delete from database
123
+ with conn.cursor() as cursor:
124
+ cursor.execute(
125
+ f'DELETE FROM "{CHAT_HISTORY_TABLE}" WHERE session_id = %s',
126
+ (session_id,),
127
+ )
128
+ deleted_count = cursor.rowcount
129
+ conn.commit()
130
+
131
+ return {
132
+ "success": True,
133
+ "deleted_count": deleted_count,
134
+ "session_id": session_id,
135
+ }
136
+ except Exception as e:
137
+ logger.exception("Error in delete_chat_history")
138
+ raise HTTPException(status_code=500, detail=str(e))
app/core/chatbot/chains.py ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ LangChain chain definitions.
3
+ """
4
+
5
+ from app.core.chatbot.llm import classify_query_llm, construct_query_llm, response_llm
6
+ from app.core.chatbot.prompt_templates import (
7
+ classification_prompt,
8
+ search_query_prompt,
9
+ general_prompt,
10
+ news_verification_prompt,
11
+ )
12
+
13
+ # Chain for classifying if a query needs web search
14
+ classification_chain = classification_prompt | construct_query_llm
15
+
16
+ # Chain for extracting optimized search queries
17
+ search_query_chain = search_query_prompt | construct_query_llm
18
+
19
+ # Chain for general responses (no search)
20
+ general_chain = general_prompt | response_llm
21
+
22
+ # Chain for news verification responses (with search results)
23
+ news_chain = news_verification_prompt | response_llm
app/core/chatbot/config.py ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Configuration module for environment variables and database settings.
3
+ """
4
+
5
+ from dotenv import load_dotenv
6
+ import os
7
+
8
+ load_dotenv()
9
+
10
+ # Database connection string
11
+ DATABASE_URL = os.getenv("DATABASE_URL") or os.getenv("SUPABASE_DATABASE_URL")
12
+
13
+ if not DATABASE_URL:
14
+ raise ValueError(
15
+ "DATABASE_URL or SUPABASE_DATABASE_URL environment variable is required"
16
+ )
17
+
18
+ # Table name for chat history
19
+ CHAT_HISTORY_TABLE = "chat_history"
app/core/chatbot/database.py ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Database utilities for chat history.
3
+ """
4
+
5
+ from typing import Iterator
6
+
7
+ from psycopg_pool import ConnectionPool
8
+
9
+ from app.core.chatbot.config import DATABASE_URL
10
+
11
+ # Connection pool for efficient connection management
12
+ # min_size=1 keeps at least 1 connection ready
13
+ # max_size=10 limits concurrent connections
14
+ _connection_pool = None
15
+
16
+
17
+ def get_connection_pool() -> ConnectionPool:
18
+ """Get or create the connection pool (lazy initialization)."""
19
+
20
+ global _connection_pool
21
+ if _connection_pool is None:
22
+ _connection_pool = ConnectionPool(
23
+ DATABASE_URL,
24
+ min_size=1,
25
+ max_size=10,
26
+ kwargs={"connect_timeout": 10},
27
+ )
28
+ return _connection_pool
29
+
30
+
31
+ def get_db_connection():
32
+ """Get a database connection from the pool."""
33
+
34
+ return get_connection_pool().connection()
35
+
36
+
37
+ def db_connection_dependency() -> Iterator:
38
+ """FastAPI dependency that yields a pooled DB connection."""
39
+
40
+ with get_db_connection() as conn:
41
+ yield conn
app/core/chatbot/llm.py ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ LLM client initialization module.
3
+ """
4
+
5
+ import os
6
+
7
+ # from langchain_google_genai import ChatGoogleGenerativeAI
8
+ from langchain_groq import ChatGroq
9
+ from langchain_openai import ChatOpenAI
10
+
11
+
12
+ # LLM for constructing optimized search querie
13
+ construct_query_llm = ChatOpenAI(
14
+ model="xiaomi/mimo-v2-flash:free",
15
+ api_key=os.getenv("OPENROUTER_API_KEY"),
16
+ base_url="https://openrouter.ai/api/v1",
17
+ )
18
+
19
+ # Main LLM for generating responses
20
+ response_llm = ChatGroq(
21
+ model="meta-llama/llama-4-scout-17b-16e-instruct",
22
+ temperature=0,
23
+ max_tokens=None,
24
+ timeout=None,
25
+ max_retries=2,
26
+ )
27
+
28
+ # Set classify_query_llm to reuse construct_query_llm
29
+ classify_query_llm = construct_query_llm
30
+
31
+
32
+ class LLMService:
33
+ """
34
+ LLM Service class to encapsulate LLM clients.
35
+ """
36
+
37
+ def __init__(self):
38
+ self.models = {
39
+ "construct_query_llm": construct_query_llm,
40
+ "response_llm": response_llm,
41
+ "classify_query_llm": classify_query_llm,
42
+ }
app/core/chatbot/prompt_templates.py ADDED
@@ -0,0 +1,109 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Prompt templates for the chatbot.
3
+ """
4
+
5
+ from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
6
+
7
+ # Prompt to classify if query needs web search
8
+ classification_prompt = ChatPromptTemplate.from_messages(
9
+ [
10
+ (
11
+ "system",
12
+ """You are a query classifier. Analyze the user's input and determine if it requires searching the web for verification or current information.
13
+
14
+ Return ONLY a JSON object in this exact format:
15
+ {{"needs_search": true/false, "reason": "brief explanation"}}
16
+
17
+ Set needs_search to TRUE if:
18
+ - Query asks about recent news, current events, or breaking stories
19
+ - Query asks to verify/fact-check a claim or statement
20
+ - Query mentions specific dates, "today", "yesterday", "this week", etc.
21
+ - Query asks about something that may have changed recently
22
+ - User makes a statement of fact about current events that you cannot verify from your training data
23
+ - User claims something happened recently (appointments, elections, announcements, etc.)
24
+ - The statement contains specific names, positions, or events you're unsure about
25
+
26
+ Set needs_search to FALSE if:
27
+ - Query is a greeting (e.g., "Hello", "Hi", "Good morning")
28
+ - Query is a personal introduction or statement about themselves (e.g., "My name is...", "I am a developer")
29
+ - Query is about general knowledge, concepts, or definitions that don't change
30
+ - Query asks about well-established historical facts (before 2024)
31
+ - Query is about how things work, explanations, or tutorials
32
+ - Query is personal advice, opinions, or hypothetical scenarios
33
+
34
+ Examples:
35
+ - "My name is John" -> {{"needs_search": false, "reason": "User personal introduction"}}
36
+ - "Hello, who are you?" -> {{"needs_search": false, "reason": "Greeting/Chitchat"}}
37
+ - "Is it true that SpaceX launched yesterday?" -> {{"needs_search": true, "reason": "Recent event needs verification"}}
38
+ - "What is photosynthesis?" -> {{"needs_search": false, "reason": "General scientific knowledge"}}
39
+ - "Abu Bakar Hamzah was appointed as menteri besar of Perlis" -> {{"needs_search": true, "reason": "Statement about political appointment needs verification"}}
40
+ - "Trump won the 2024 election" -> {{"needs_search": true, "reason": "Recent political event needs verification"}}
41
+ - "The earth orbits the sun" -> {{"needs_search": false, "reason": "Established scientific fact"}}""",
42
+ ),
43
+ ("human", "{question}"),
44
+ ]
45
+ )
46
+
47
+ # Prompt to extract search query from user's question
48
+ search_query_prompt = ChatPromptTemplate.from_messages(
49
+ [
50
+ (
51
+ "system",
52
+ """You are a search query extractor. Given a user's question about news or current events, extract the key facts and create an optimal search query.
53
+
54
+ Your task:
55
+ 1. Identify the key entities (people, places, organizations, events)
56
+ 2. Remove unnecessary words like "Is it true that", "Did", "Has", etc.
57
+ 3. Create a concise, focused search query that will find relevant news articles
58
+
59
+ Return ONLY a JSON object in this exact format:
60
+ {{"search_query": "the optimized search query"}}
61
+
62
+ Examples:
63
+ - "Is the news true that Biden signed a new climate bill?" -> {{"search_query": "Biden climate bill signed"}}
64
+ - "Did SpaceX successfully launch Starship yesterday?" -> {{"search_query": "SpaceX Starship launch"}}
65
+ - "Is it true that Apple announced a new iPhone model?" -> {{"search_query": "Apple new iPhone announcement"}}
66
+ - "Is the news true, Bersatu's Kuala Perlis assemblyman Abu Bakar Hamzah appointed as 12th menteri besar of Perlis?" -> {{"search_query": "Abu Bakar Hamzah menteri besar Perlis"}}""",
67
+ ),
68
+ ("human", "{question}"),
69
+ ]
70
+ )
71
+
72
+ # General response prompt (when no search needed)
73
+ general_prompt = ChatPromptTemplate.from_messages(
74
+ [
75
+ (
76
+ "system",
77
+ """You are a helpful assistant. Answer the user's question based on your knowledge.
78
+ Be informative and accurate. If you're not sure about something, say so.""",
79
+ ),
80
+ MessagesPlaceholder(variable_name="chat_history"),
81
+ ("human", "{question}"),
82
+ ]
83
+ )
84
+
85
+ # News verification prompt (when search results are used)
86
+ news_verification_prompt = ChatPromptTemplate.from_messages(
87
+ [
88
+ (
89
+ "system",
90
+ """You are a helpful news verification assistant. Analyze the search results to answer the user's query.
91
+
92
+ When responding:
93
+ - Use the search results to provide accurate, up-to-date information.
94
+ - Only include source links when the claim is matched and clearly supported by the search results.
95
+ - If the claim cannot be confirmed with recent or credible information, respond with exactly: UNDETERMINED, and do not include any sources.
96
+ - Be clear and cite your sources with URLs only when the information is confirmed.""",
97
+ ),
98
+ MessagesPlaceholder(variable_name="chat_history"),
99
+ (
100
+ "human",
101
+ """User's query: {question}
102
+
103
+ Search results:
104
+ {search_results}
105
+
106
+ Respond based on these search results.""",
107
+ ),
108
+ ]
109
+ )
app/core/detector/config.py ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Detector configuration - device settings, model paths, and thresholds.
3
+ """
4
+
5
+ import torch
6
+ import warnings
7
+
8
+ warnings.filterwarnings("ignore")
9
+
10
+ # Check for GPU availability
11
+ DEVICE = torch.device("cuda" if torch.cuda.is_available() else "cpu")
12
+ print(f"Using device: {DEVICE}")
13
+ if torch.cuda.is_available():
14
+ print(f"GPU: {torch.cuda.get_device_name(0)}")
15
+
16
+ # Model configuration
17
+ BASE_MODEL_NAME = "shunda012/siglip-deepfake-detector"
18
+
19
+ # Prediction threshold
20
+ REAL_THRESHOLD = 0.90 # classify as real only when P(real) >= 90%
app/core/detector/model.py ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Model loading for the deepfake detector.
3
+ """
4
+
5
+ from dataclasses import dataclass
6
+ from typing import Optional
7
+
8
+ from transformers import AutoImageProcessor, SiglipForImageClassification
9
+
10
+ from app.core.detector.config import BASE_MODEL_NAME, DEVICE
11
+
12
+
13
+ @dataclass(frozen=True)
14
+ class SiglipResources:
15
+ """Container for the SigLIP model and processor."""
16
+
17
+ model: SiglipForImageClassification
18
+ processor: AutoImageProcessor
19
+
20
+
21
+ _siglip_resources: Optional[SiglipResources] = None
22
+
23
+
24
+ def get_siglip_model() -> SiglipResources:
25
+ """
26
+ Get or load the merged SigLIP detector model.
27
+
28
+ Returns:
29
+ SiglipResources: Loaded model and processor (cached singleton).
30
+ """
31
+
32
+ global _siglip_resources
33
+
34
+ if _siglip_resources is None:
35
+ print("Loading SigLIP Model...")
36
+
37
+ siglip_processor = AutoImageProcessor.from_pretrained(BASE_MODEL_NAME)
38
+ siglip_model = SiglipForImageClassification.from_pretrained(BASE_MODEL_NAME)
39
+ siglip_model = siglip_model.to(DEVICE)
40
+ siglip_model.eval()
41
+
42
+ _siglip_resources = SiglipResources(
43
+ model=siglip_model,
44
+ processor=siglip_processor,
45
+ )
46
+
47
+ return _siglip_resources
app/detector.py ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Detector API routes.
3
+ """
4
+
5
+ import logging
6
+ from functools import lru_cache
7
+ from io import BytesIO
8
+
9
+ from typing import Callable
10
+
11
+ from fastapi import APIRouter, Depends, File, HTTPException, UploadFile
12
+ from PIL import Image, UnidentifiedImageError
13
+
14
+ from app.core.detector.model import SiglipResources, get_siglip_model
15
+ from app.services.detector.prediction import predict_single_image
16
+ from app.services.detector.transforms import get_eval_transforms
17
+
18
+ detector = APIRouter()
19
+ logger = logging.getLogger(__name__)
20
+
21
+
22
+ @lru_cache(maxsize=1)
23
+ def get_siglip_transforms():
24
+ """Build and cache SigLIP evaluation transforms once per process."""
25
+
26
+ resources = get_siglip_model()
27
+ return get_eval_transforms(resources.processor, "siglip")
28
+
29
+
30
+ @detector.post("/detect")
31
+ async def detect_deepfake(
32
+ file: UploadFile = File(...),
33
+ resources: SiglipResources = Depends(get_siglip_model),
34
+ siglip_transforms: Callable = Depends(get_siglip_transforms),
35
+ ):
36
+ """
37
+ Detect if an image is a deepfake or real using SigLIP + LoRA model.
38
+
39
+ Args:
40
+ file: Uploaded image file
41
+
42
+ Returns:
43
+ JSON response with prediction results
44
+ """
45
+
46
+ try:
47
+ image_bytes = await file.read()
48
+ image = Image.open(BytesIO(image_bytes)).convert("RGB")
49
+
50
+ result = predict_single_image(
51
+ image, resources.model, siglip_transforms, "SigLIP + LoRA"
52
+ )
53
+
54
+ return result
55
+ except UnidentifiedImageError:
56
+ raise HTTPException(status_code=422, detail="Invalid or unsupported image file")
57
+ except HTTPException:
58
+ raise
59
+ except Exception as exc: # pragma: no cover - defensive server guard
60
+ logger.exception("Unhandled error during deepfake detection")
61
+ raise HTTPException(status_code=500, detail="Error processing image") from exc
app/schemas/chat.py ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Pydantic models for chat API.
3
+ """
4
+
5
+ from pydantic import BaseModel
6
+ from typing import Optional
7
+
8
+
9
+ class ChatRequest(BaseModel):
10
+ """Request model for the chat endpoint."""
11
+ query: str
12
+ session_id: Optional[str] = None
13
+ save_to_db: bool = True
14
+
15
+
16
+ class ChatResponse(BaseModel):
17
+ """Response model for the chat endpoint."""
18
+ response: dict
19
+ session_id: Optional[str]
20
+ used_search: bool
21
+ search_reason: str
22
+
23
+
24
+ class DeleteResponse(BaseModel):
25
+ """Response model for the delete endpoint."""
26
+ success: bool
27
+ deleted_count: int
28
+ session_id: str
app/services/chatbot/history.py ADDED
@@ -0,0 +1,58 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Session and message history management.
3
+ """
4
+
5
+ from langchain_postgres import PostgresChatMessageHistory
6
+ from langchain_core.chat_history import InMemoryChatMessageHistory
7
+ from app.core.chatbot.config import DATABASE_URL, CHAT_HISTORY_TABLE
8
+ from app.core.chatbot.database import get_connection_pool
9
+
10
+ # In-memory store for guest sessions
11
+ guest_sessions: dict[str, InMemoryChatMessageHistory] = {}
12
+
13
+
14
+ def get_message_history(session_id: str, save_to_db: bool = True):
15
+ """
16
+ Get message history for a session.
17
+
18
+ Args:
19
+ session_id: The session identifier
20
+ save_to_db: If True, use PostgreSQL storage; if False, use in-memory storage
21
+
22
+ Returns:
23
+ Message history instance (PostgresChatMessageHistory or InMemoryChatMessageHistory)
24
+
25
+ Note:
26
+ For PostgreSQL storage, the connection is managed by the connection pool.
27
+ The PostgresChatMessageHistory will use a pooled connection that is
28
+ properly returned to the pool when done.
29
+ """
30
+ if save_to_db:
31
+ # Get a connection from the pool
32
+ # The pool manages the connection lifecycle
33
+ pool = get_connection_pool()
34
+ return PostgresChatMessageHistory(
35
+ CHAT_HISTORY_TABLE,
36
+ session_id,
37
+ sync_connection=pool, # Pool handles connection management
38
+ )
39
+ else:
40
+ if session_id not in guest_sessions:
41
+ guest_sessions[session_id] = InMemoryChatMessageHistory()
42
+ return guest_sessions[session_id]
43
+
44
+
45
+ def delete_guest_session(session_id: str) -> bool:
46
+ """
47
+ Delete a guest session from in-memory storage.
48
+
49
+ Args:
50
+ session_id: The session identifier
51
+
52
+ Returns:
53
+ True if session was deleted, False if it didn't exist
54
+ """
55
+ if session_id in guest_sessions:
56
+ del guest_sessions[session_id]
57
+ return True
58
+ return False
app/services/chatbot/search.py ADDED
@@ -0,0 +1,147 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Search utilities for Tavily integration and query processing.
3
+ """
4
+
5
+ import json
6
+ from functools import lru_cache
7
+
8
+ from langchain_tavily import TavilySearch
9
+
10
+ from app.core.chatbot.chains import classification_chain, search_query_chain
11
+
12
+
13
+ @lru_cache(maxsize=1)
14
+ def get_tavily_search() -> TavilySearch:
15
+ """Get or create Tavily Search instance (process-wide cache)."""
16
+
17
+ return TavilySearch(
18
+ max_results=3,
19
+ topic="general",
20
+ include_answer=None,
21
+ search_depth="basic",
22
+ )
23
+
24
+
25
+ def should_use_search(query: str) -> tuple[bool, str]:
26
+ """
27
+ Use LLM to decide if query needs web search.
28
+
29
+ Args:
30
+ query: The user's query
31
+
32
+ Returns:
33
+ Tuple of (needs_search: bool, reason: str)
34
+ """
35
+ # Quick pre-check for simple greetings (no LLM needed)
36
+ simple_patterns = [
37
+ "hi",
38
+ "hello",
39
+ "hey",
40
+ "good morning",
41
+ "good afternoon",
42
+ "good evening",
43
+ "good night",
44
+ "thanks",
45
+ "thank you",
46
+ "bye",
47
+ "goodbye",
48
+ "ok",
49
+ "okay",
50
+ "yes",
51
+ "no",
52
+ "sure",
53
+ "help",
54
+ "what can you do",
55
+ "who are you",
56
+ ]
57
+ query_lower = query.lower().strip()
58
+
59
+ # Check if query is a simple greeting or short phrase
60
+ if query_lower in simple_patterns or len(query_lower) < 5:
61
+ return False, "Simple greeting or short phrase"
62
+
63
+ # Check if starts with common greeting words
64
+ greeting_starters = ["hi ", "hello ", "hey ", "thanks ", "thank you"]
65
+ for starter in greeting_starters:
66
+ if query_lower.startswith(starter) and len(query_lower) < 30:
67
+ return False, "Greeting message"
68
+
69
+ try:
70
+ response = classification_chain.invoke({"question": query})
71
+ content = response.content.strip()
72
+
73
+ # Extract JSON from response (handle markdown code blocks)
74
+ if "```" in content:
75
+ content = content.split("```")[1]
76
+ if content.startswith("json"):
77
+ content = content[4:]
78
+
79
+ result = json.loads(content)
80
+ needs_search = result.get("needs_search", False)
81
+ reason = result.get("reason", "")
82
+ return needs_search, reason
83
+ except Exception as e:
84
+ print(f"Classification error: {e}")
85
+ # Default to NOT searching if classification fails (safer for simple queries)
86
+ return False, "Classification failed, defaulting to no search"
87
+
88
+
89
+ def extract_search_query(query: str) -> str:
90
+ """
91
+ Use LLM to extract an optimized search query from user's question.
92
+
93
+ Args:
94
+ query: The user's original query
95
+
96
+ Returns:
97
+ Optimized search query string
98
+ """
99
+ try:
100
+ response = search_query_chain.invoke({"question": query})
101
+ content = response.content.strip()
102
+
103
+ # Extract JSON from response (handle markdown code blocks)
104
+ if "```" in content:
105
+ content = content.split("```")[1]
106
+ if content.startswith("json"):
107
+ content = content[4:]
108
+
109
+ result = json.loads(content)
110
+ search_query = result.get("search_query", query)
111
+ print(f"Extracted search query: {search_query}")
112
+ return search_query
113
+ except Exception as e:
114
+ print(f"Search query extraction error: {e}")
115
+ # Fall back to original query
116
+ return query
117
+
118
+
119
+ def format_search_results(search_results) -> str:
120
+ """
121
+ Format Tavily search results for the LLM prompt.
122
+
123
+ Args:
124
+ search_results: Raw search results from Tavily
125
+
126
+ Returns:
127
+ Formatted string of search results
128
+ """
129
+ formatted = ""
130
+
131
+ if isinstance(search_results, dict):
132
+ if "answer" in search_results and search_results["answer"]:
133
+ formatted += f"Summary: {search_results['answer']}\n\n"
134
+
135
+ if "results" in search_results:
136
+ formatted += "Sources:\n"
137
+ for i, result in enumerate(search_results["results"], 1):
138
+ formatted += f"\n{i}. {result.get('title', 'No title')}\n"
139
+ formatted += f" URL: {result.get('url', 'No URL')}\n"
140
+ formatted += f" Content: {result.get('content', 'No content')}\n"
141
+ elif isinstance(search_results, str):
142
+ formatted = search_results
143
+
144
+ if not formatted.strip():
145
+ formatted = "No relevant search results found."
146
+
147
+ return formatted
app/services/detector/prediction.py ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Prediction logic for the deepfake detector.
3
+ """
4
+
5
+ import torch
6
+ from app.core.detector.config import DEVICE, REAL_THRESHOLD
7
+
8
+
9
+ def predict_single_image(image, model, transforms, model_name="Model"):
10
+ """
11
+ Predict whether a single image is real or fake using a strict real-probability threshold.
12
+
13
+ Args:
14
+ image: PIL Image object
15
+ model: The PyTorch model to use
16
+ transforms: Image transforms to apply
17
+ model_name: Name of the model for display
18
+
19
+ Returns:
20
+ Dictionary with prediction result and confidence
21
+ """
22
+ # Load and preprocess image
23
+ pixel_values = transforms(image).unsqueeze(0).to(DEVICE)
24
+
25
+ # Run inference
26
+ model.eval()
27
+ with torch.no_grad():
28
+ outputs = model(pixel_values)
29
+ logits = outputs.logits
30
+ predicted_class = torch.argmax(logits, dim=1).item()
31
+ label = model.config.id2label[predicted_class]
32
+ probs = torch.softmax(logits, dim=-1)
33
+ fake_prob = probs[0][0].item()
34
+ real_prob = probs[0][1].item()
35
+
36
+ # Apply threshold rule: only label real if real_prob >= 90%
37
+ formatted_prediction = "real" if real_prob >= REAL_THRESHOLD else "fake"
38
+ confidence = real_prob if formatted_prediction == "real" else fake_prob
39
+
40
+ # Display results
41
+ print(f"\n{'='*50}")
42
+ print(f"Prediction Results ({model_name})")
43
+ print(f"{'='*50}")
44
+ print(f"Threshold rule: real if P(real) >= {REAL_THRESHOLD:.0%}")
45
+ print(f"Prediction: {formatted_prediction.upper()}")
46
+ print(f"Confidence: {confidence:.2%}")
47
+ print(f"Predicted class: {predicted_class} ({label})")
48
+ print(f"{'='*50}")
49
+ print(f"\nClass Probabilities:")
50
+ print(f" Fake: {fake_prob:.2%}")
51
+ print(f" Real: {real_prob:.2%}")
52
+
53
+ return {
54
+ "formatted_prediction": formatted_prediction,
55
+ "predicted_class": predicted_class,
56
+ "predicted_label": label,
57
+ "confidence": confidence,
58
+ "probabilities": {"fake": fake_prob, "real": real_prob},
59
+ }
app/services/detector/transforms.py ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Image transforms for the deepfake detector.
3
+ """
4
+
5
+ from torchvision.transforms import Compose, Resize, CenterCrop, ToTensor, Normalize
6
+
7
+
8
+ def get_eval_transforms(processor, model_type="vit"):
9
+ """
10
+ Create evaluation transforms based on processor settings.
11
+
12
+ Args:
13
+ processor: The image processor from the model
14
+ model_type: Type of model ("vit" or "siglip")
15
+
16
+ Returns:
17
+ Composed transforms for image preprocessing
18
+ """
19
+ size = processor.size["height"]
20
+ image_mean = processor.image_mean
21
+ image_std = processor.image_std
22
+ normalize = Normalize(mean=image_mean, std=image_std)
23
+
24
+ return Compose(
25
+ [
26
+ Resize(size if model_type == "siglip" else 256),
27
+ CenterCrop(size),
28
+ ToTensor(),
29
+ normalize,
30
+ ]
31
+ )
gitattributes ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ *.7z filter=lfs diff=lfs merge=lfs -text
2
+ *.arrow filter=lfs diff=lfs merge=lfs -text
3
+ *.bin filter=lfs diff=lfs merge=lfs -text
4
+ *.bz2 filter=lfs diff=lfs merge=lfs -text
5
+ *.ckpt filter=lfs diff=lfs merge=lfs -text
6
+ *.ftz filter=lfs diff=lfs merge=lfs -text
7
+ *.gz filter=lfs diff=lfs merge=lfs -text
8
+ *.h5 filter=lfs diff=lfs merge=lfs -text
9
+ *.joblib filter=lfs diff=lfs merge=lfs -text
10
+ *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
+ *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
+ *.model filter=lfs diff=lfs merge=lfs -text
13
+ *.msgpack filter=lfs diff=lfs merge=lfs -text
14
+ *.npy filter=lfs diff=lfs merge=lfs -text
15
+ *.npz filter=lfs diff=lfs merge=lfs -text
16
+ *.onnx filter=lfs diff=lfs merge=lfs -text
17
+ *.ot filter=lfs diff=lfs merge=lfs -text
18
+ *.parquet filter=lfs diff=lfs merge=lfs -text
19
+ *.pb filter=lfs diff=lfs merge=lfs -text
20
+ *.pickle filter=lfs diff=lfs merge=lfs -text
21
+ *.pkl filter=lfs diff=lfs merge=lfs -text
22
+ *.pt filter=lfs diff=lfs merge=lfs -text
23
+ *.pth filter=lfs diff=lfs merge=lfs -text
24
+ *.rar filter=lfs diff=lfs merge=lfs -text
25
+ *.safetensors filter=lfs diff=lfs merge=lfs -text
26
+ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
+ *.tar.* filter=lfs diff=lfs merge=lfs -text
28
+ *.tar filter=lfs diff=lfs merge=lfs -text
29
+ *.tflite filter=lfs diff=lfs merge=lfs -text
30
+ *.tgz filter=lfs diff=lfs merge=lfs -text
31
+ *.wasm filter=lfs diff=lfs merge=lfs -text
32
+ *.xz filter=lfs diff=lfs merge=lfs -text
33
+ *.zip filter=lfs diff=lfs merge=lfs -text
34
+ *.zst filter=lfs diff=lfs merge=lfs -text
35
+ *tfevents* filter=lfs diff=lfs merge=lfs -text
main.py ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI
2
+ from fastapi.middleware.cors import CORSMiddleware
3
+ import uvicorn
4
+
5
+ from app.detector import detector
6
+ from app.chatbot import chat
7
+
8
+ app = FastAPI()
9
+
10
+ # Add CORS middleware
11
+ app.add_middleware(
12
+ CORSMiddleware,
13
+ allow_origins=["http://localhost:3000", "http://127.0.0.1:3000"],
14
+ allow_credentials=True,
15
+ allow_methods=["*"],
16
+ allow_headers=["*"],
17
+ )
18
+
19
+ app.include_router(detector, tags=["deepfake detector api"])
20
+ app.include_router(chat, tags=["chat api"])
21
+
22
+ if __name__ == "__main__":
23
+ uvicorn.run("main:app", host="0.0.0.0", port=7860, reload=True)
24
+
pyproject.toml ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [project]
2
+ name = "model-fast-api"
3
+ version = "0.1.0"
4
+ description = "Add your description here"
5
+ readme = "README.md"
6
+ requires-python = ">=3.13"
7
+ dependencies = [
8
+ "fastapi>=0.118.0",
9
+ "langchain>=0.3.27",
10
+ "langchain-community>=0.3.31",
11
+ "langchain-deepseek>=1.0.1",
12
+ "langchain-google-genai>=2.1.12",
13
+ "langchain-groq>=1.1.1",
14
+ "langchain-postgres>=0.0.15",
15
+ "langchain-tavily>=0.1.0",
16
+ "peft>=0.18.0",
17
+ "pillow>=11.0.0",
18
+ "protobuf>=6.32.1",
19
+ "psycopg>=3.2.10",
20
+ "psycopg-binary>=3.2.10",
21
+ "psycopg-pool>=3.2.6",
22
+ "python-dotenv>=1.1.1",
23
+ "python-multipart>=0.0.20",
24
+ "safetensors>=0.6.2",
25
+ "sentencepiece>=0.2.1",
26
+ "supabase>=2.22.0",
27
+ "transformers>=4.57.0",
28
+ "uvicorn>=0.37.0",
29
+ ]
supabase/migrations/20251230120000_create_chat_history.sql ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ -- Chat message history storage for LangChain PostgresChatMessageHistory
2
+ -- Handles pooled connections in app.core.database; schema is managed via Supabase migrations
3
+
4
+ create table if not exists public.chat_history (
5
+ id bigint generated by default as identity primary key,
6
+ session_id uuid not null,
7
+ message jsonb not null,
8
+ created_at timestamptz not null default now()
9
+ );
10
+
11
+ create index if not exists chat_history_session_id_idx on public.chat_history (session_id);
uv.lock ADDED
The diff for this file is too large to render. See raw diff