Spaces:
Sleeping
Sleeping
| #!/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 | |
| ) | |