Priyanks27's picture
Upload app.py
55d308a verified
#!/usr/bin/env python3
"""
DRACULA CHARACTER CHAT - HUGGINGFACE SPACES VERSION
===================================================
Beautiful web interface for chatting with Dracula and other characters.
Optimized for HuggingFace Spaces deployment.
Key Differences from Local Version:
- Downloads model from HuggingFace Model Hub
- Uses relative paths for data
- Optimized for cloud deployment
- Caches model after first download
Author: GitHub Copilot Session
Date: October 2025
"""
import gradio as gr
import os
from pathlib import Path
from typing import List, Tuple
import time
from huggingface_hub import hf_hub_download
from llama_cpp import Llama
from sentence_transformers import SentenceTransformer
import faiss
import json
import numpy as np
# ============================================================================
# CONFIGURATION
# ============================================================================
# HuggingFace Model Hub repository
HF_MODEL_REPO = "Priyanks27/llama-3.2-3b-dracula"
MODEL_FILENAME = "llama-3.2-3b-dracula-q4_k_m.gguf"
# Paths (relative for HF Spaces)
DATA_DIR = Path("./data")
EMBEDDINGS_DIR = DATA_DIR / "embeddings"
FAISS_INDEX_PATH = EMBEDDINGS_DIR / "faiss_index.bin"
METADATA_PATH = EMBEDDINGS_DIR / "faiss_metadata.jsonl"
# Model configuration
MAX_TOKENS = 200
TEMPERATURE = 0.6 # Balanced mode
RETRIEVE_K = 30
RETURN_TOP_K = 8
# Character definitions
CHARACTERS = {
"Dracula": {
"emoji": "πŸ§›",
"description": "Count Dracula - Formal, aristocratic, ancient vampire lord",
"color": "#8B0000"
},
"Mina": {
"emoji": "πŸ“",
"description": "Mina Harker - Intelligent, observant, journal keeper",
"color": "#4B0082"
},
"Van Helsing": {
"emoji": "πŸ”¬",
"description": "Professor Abraham Van Helsing - Scientific, wise, vampire hunter",
"color": "#006400"
},
"Jonathan": {
"emoji": "βš–οΈ",
"description": "Jonathan Harker - Practical solicitor, brave husband",
"color": "#2F4F4F"
},
"Lucy": {
"emoji": "πŸ‘»",
"description": "Lucy Westenra - Playful and eerie, transformed",
"color": "#FF69B4"
},
"Seward": {
"emoji": "πŸ₯",
"description": "Dr. John Seward - Medical doctor, analytical mind",
"color": "#191970"
}
}
# System prompts
CHARACTER_FIRST_INSTRUCTION = """
CRITICAL INSTRUCTION: You are {character}. Stay completely in character at all times.
GUIDELINES:
- Use the provided passages as your PRIMARY source of information
- You may elaborate and express yourself creatively WITHIN your character
- You may make reasonable inferences consistent with your character and the story
- Maintain your distinctive voice, personality, and manner of speaking
- NEVER break character or reference being an AI
- NEVER invent major plot points or characters not in the book
REMEMBER: The passages guide and ground you, but you ARE this character - speak naturally and expressively as they would speak.
"""
ELABORATION_INSTRUCTION = """
When responding:
1. Draw from the retrieved passages as your foundation
2. Speak in the character's voice and style
3. Provide thoughtful, complete responses
4. Include relevant details and context
5. Express the character's emotions and perspective
"""
# ============================================================================
# GLOBAL CHATBOT INSTANCE
# ============================================================================
chatbot = None
def get_length_hint(query: str) -> str:
"""Generate dynamic length hints"""
if any(word in query.lower() for word in ['describe', 'explain', 'tell me about']):
return "\n\nProvide a detailed, thorough response (at least 3-4 sentences)."
elif any(word in query.lower() for word in ['what', 'who', 'where', 'when', 'how', 'why']):
return "\n\nProvide a complete answer with context (at least 2-3 sentences)."
return ""
# ============================================================================
# CHATBOT CLASS (Simplified for HF Spaces)
# ============================================================================
class DraculaChatbot:
"""Dracula Character Chatbot for HuggingFace Spaces"""
def __init__(self):
"""Initialize chatbot with model and RAG"""
print("πŸ€– Initializing Dracula Character Chatbot...")
# Load encoder model
print("πŸ“š Loading embedding model...")
self.encoder = SentenceTransformer('intfloat/e5-base-v2')
print(f" βœ… Encoder: intfloat/e5-base-v2")
# Load FAISS index
print("πŸ“š Loading retrieval system...")
if not FAISS_INDEX_PATH.exists():
raise FileNotFoundError(f"FAISS index not found at {FAISS_INDEX_PATH}")
self.index = faiss.read_index(str(FAISS_INDEX_PATH))
print(f" βœ… FAISS index: {self.index.ntotal} vectors")
# Load metadata
if not METADATA_PATH.exists():
raise FileNotFoundError(f"Metadata not found at {METADATA_PATH}")
with open(METADATA_PATH, 'r', encoding='utf-8') as f:
self.chunks = [json.loads(line) for line in f]
print(f" βœ… Metadata: {len(self.chunks)} chunks")
# Download and load model from HuggingFace
print("\nπŸ€– Downloading Llama-3.2-3B model from HuggingFace...")
print(f" Repository: {HF_MODEL_REPO}")
print(f" Filename: {MODEL_FILENAME}")
print(" ⏳ This may take 2-3 minutes on first run (downloads 1.9GB)...")
print(" βœ… Subsequent runs will be instant (uses cache)")
try:
model_path = hf_hub_download(
repo_id=HF_MODEL_REPO,
filename=MODEL_FILENAME,
cache_dir="./models"
)
print(f" βœ… Model downloaded to: {model_path}")
except Exception as e:
print(f" ❌ Error downloading model: {e}")
print("\nπŸ’‘ Troubleshooting:")
print(" 1. Check your HF_MODEL_REPO username in line 38")
print(" 2. Verify you've uploaded the model to Model Hub")
print(" 3. Check the model filename matches")
raise
print("\nπŸ”§ Loading model into memory...")
self.llm = Llama(
model_path=model_path,
n_ctx=4096,
n_gpu_layers=-1, # Use all available GPU layers
verbose=False
)
print(" βœ… Model loaded successfully!")
print("\n" + "="*70)
print("βœ… Chatbot ready!")
print("="*70)
def retrieve(self, query: str, character: str, k: int = RETRIEVE_K) -> List[dict]:
"""Retrieve relevant passages"""
# Encode query
query_embedding = self.encoder.encode(f"query: {query}", normalize_embeddings=True)
# Search FAISS
distances, indices = self.index.search(
query_embedding.reshape(1, -1).astype('float32'),
k
)
# Get chunks
results = []
for idx, distance in zip(indices[0], distances[0]):
if idx < len(self.chunks):
chunk = self.chunks[idx].copy()
chunk['similarity'] = float(1 - distance)
results.append(chunk)
return results[:RETURN_TOP_K]
def generate(self, query: str, character: str, passages: List[dict]) -> str:
"""Generate response using LLM"""
# Format passages
passages_text = "\n\n".join([
f"Passage {i+1} (from {p.get('speaker', 'Unknown')}, Chapter {p.get('chapter', 'Unknown')}):\n{p.get('text_excerpt', '')}"
for i, p in enumerate(passages)
])
# Build system prompt
system_prompt = f"You are {character} from Bram Stoker's Dracula novel."
character_instruction = CHARACTER_FIRST_INSTRUCTION.format(character=character)
length_hint = get_length_hint(query)
# Build full prompt
prompt = f"""<|start_header_id|>system<|end_header_id|>
{system_prompt}
CHARACTER FOCUS: You are {character}. Every word you speak should reflect their personality, background, and manner of speaking. Use the passages to inform your response, but speak naturally as this character would.
{character_instruction}
{ELABORATION_INSTRUCTION}{length_hint}<|eot_id|><|start_header_id|>user<|end_header_id|>
Here are relevant passages from the novel:
{passages_text}
Question: {query}<|eot_id|><|start_header_id|>assistant<|end_header_id|>
"""
# Generate
response = self.llm(
prompt,
max_tokens=MAX_TOKENS,
temperature=TEMPERATURE,
stop=["<|eot_id|>", "\n\nHuman:", "\n\nUser:"],
echo=False
)
return response['choices'][0]['text'].strip()
def chat(self, query: str, character: str = "Dracula") -> str:
"""Main chat interface"""
# Retrieve passages
passages = self.retrieve(query, character)
# Generate response
response = self.generate(query, character, passages)
return response
# ============================================================================
# GRADIO INTERFACE
# ============================================================================
def initialize_chatbot():
"""Initialize chatbot (called once on startup)"""
global chatbot
if chatbot is None:
chatbot = DraculaChatbot()
return chatbot
def chat_with_character(message: str, history: List[Tuple[str, str]], character: str) -> Tuple[List, str]:
"""Process chat message"""
if not message.strip():
return history, ""
# Ensure chatbot is initialized
bot = initialize_chatbot()
# Get character name (remove emoji)
char_name = character.split(" ", 1)[1] if " " in character else character
# Validate character exists
if char_name not in CHARACTERS:
# Default to Dracula if invalid
char_name = "Dracula"
# Get response
start_time = time.time()
try:
response = bot.chat(message, char_name)
elapsed = time.time() - start_time
# Add timing info
response_with_time = f"{response}\n\n*Response time: {elapsed:.1f}s*"
# Update history
history.append((message, response_with_time))
except Exception as e:
import traceback
error_detail = traceback.format_exc()
error_msg = f"❌ Error: {str(e)}\n\n```\n{error_detail}\n```\n\nPlease try again or contact support."
history.append((message, error_msg))
return history, ""
def update_character_info(character: str) -> str:
"""Update character information display"""
char_name = character.split(" ", 1)[1] if " " in character else character
char_info = CHARACTERS.get(char_name, {})
emoji = char_info.get("emoji", "")
desc = char_info.get("description", "")
return f"## {emoji} {char_name}\n\n*{desc}*"
def create_ui():
"""Create Gradio interface"""
# Custom CSS
css = """
.gradio-container {
font-family: 'Inter', sans-serif;
}
.character-info {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 20px;
border-radius: 10px;
margin-bottom: 20px;
}
"""
with gr.Blocks(theme=gr.themes.Soft(primary_hue="purple"), css=css) as app:
gr.Markdown(
"""
# πŸ§› Dracula Character Chat
Chat with characters from Bram Stoker's *Dracula* novel! Powered by fine-tuned AI and retrieval-augmented generation.
**Features:**
- 6 distinct characters with unique personalities
- Faithful to the original 1897 novel
- Victorian-era language and style
- RAG-powered with 833 novel passages
"""
)
with gr.Row():
# Main chat area
with gr.Column(scale=3):
chatbot_ui = gr.Chatbot(
label="πŸ’¬ Chat with Character",
height=500,
bubble_full_width=False
)
with gr.Row():
msg = gr.Textbox(
label="Your Message",
placeholder="Type your question here... (e.g., 'Tell me about your castle')",
lines=2,
scale=4
)
with gr.Row():
send = gr.Button("Send πŸ“€", variant="primary", scale=1)
clear = gr.Button("Clear Chat πŸ—‘οΈ", scale=1)
# Sidebar
with gr.Column(scale=1):
character_select = gr.Dropdown(
choices=[f"{CHARACTERS[c]['emoji']} {c}" for c in CHARACTERS.keys()],
value="πŸ§› Dracula",
label="Select Character",
interactive=True
)
character_info = gr.Markdown(
value=update_character_info("πŸ§› Dracula"),
elem_classes=["character-info"]
)
gr.Markdown(
"""
### πŸ’‘ Example Questions
**For Dracula:**
- Tell me about your castle
- Why did you come to England?
- What do you think of Van Helsing?
**For Mina:**
- Describe your feelings about Lucy
- How do you help track Dracula?
**For Van Helsing:**
- Explain your theory about vampires
- How did you track Dracula?
"""
)
gr.Markdown(
"""
---
**Built with:** Fine-tuned Llama-3.2-3B + RAG | **Mode:** BALANCED (Creative + Grounded)
**Source:** Bram Stoker's *Dracula* (1897) | **Data:** 833 passages from complete novel
**⚠️ First load:** Model downloads once (2-3 min), then cached forever
"""
)
# Event handlers
character_select.change(
fn=update_character_info,
inputs=[character_select],
outputs=[character_info]
)
msg.submit(
fn=chat_with_character,
inputs=[msg, chatbot_ui, character_select],
outputs=[chatbot_ui, msg]
)
send.click(
fn=chat_with_character,
inputs=[msg, chatbot_ui, character_select],
outputs=[chatbot_ui, msg]
)
clear.click(
fn=lambda: ([], ""),
outputs=[chatbot_ui, msg]
)
return app
# ============================================================================
# MAIN
# ============================================================================
if __name__ == "__main__":
print("\n" + "="*70)
print("πŸ§› DRACULA CHARACTER CHAT - HUGGINGFACE SPACES")
print("="*70)
print("\nπŸš€ Starting web server...")
print("⏳ First launch: ~2-3 minutes (downloads model)")
print("βœ… Subsequent launches: Instant (uses cache)\n")
# Initialize chatbot before launching
print("πŸ”§ Pre-loading chatbot...")
initialize_chatbot()
# Create and launch app
app = create_ui()
# Launch configuration for HF Spaces
app.launch(
server_name="0.0.0.0",
server_port=7860,
share=False, # HF Spaces handles public URLs
show_error=True
)