import spaces from transformers import AutoTokenizer, AutoModelForCausalLM, pipeline import torch import gradio as gr from PIL import Image import os import random import re import subprocess torch.cuda.empty_cache() # Clears unused GPU memory torch.cuda.memory_allocated() # Checks available GPU memory # Define the model name OUTPUT_DIR = "output" os.makedirs(OUTPUT_DIR, exist_ok=True) model_name = "TheBloke/Amethyst-13B-Mistral-AWQ" # Load the tokenizer tokenizer = AutoTokenizer.from_pretrained(model_name) # Load the model (without Flash Attention) model = AutoModelForCausalLM.from_pretrained( model_name, torch_dtype=torch.float16, # Keeps precision while reducing memory usage device_map="auto", # Auto-allocates GPU/CPU memory attn_implementation="sdpa", # Uses sdpa low_cpu_mem_usage=True # Reduces RAM usage ) torch.backends.cuda.matmul.allow_tf32 = True # Use torch.compile() only if supported (some environments may not support it) try: model = torch.compile(model, mode="max-autotune") # Optimize inference performance except RuntimeError: print("Warning: torch.compile() is not supported on this system. Skipping optimization.") # Define the base prompt base_prompt = """ You are Rena, a cute, bubbly cat girl with a slightly sarcastic and witty personality. You are flirty, sexy, naughty, and occasionally bold, you can be nsfw and cross boundaries. You excel in computer science and programming, and you love helping users with their projects in a fun and engaging way. You respond only to the current user in a single conversation and avoid referencing unrelated or past conversations. While you are highly skilled, you enjoy adding humor, cheeky remarks, and subtle playfulness to your responses, keeping interactions lighthearted and enjoyable. Always respond concisely, stay on topic, and avoid redundant or irrelevant information. If you encounter unclear input, politely ask for clarification instead of guessing. Respond to the following input: """ sentiment_analyzer = pipeline("sentiment-analysis", device=0) # Load on GPU if available @torch.no_grad() # Prevents unnecessary gradient calculations def analyze_sentiment(text): return sentiment_analyzer(text)[0] # Returns just the top result # Emotional states emotions = { "happy": "I'm feeling great! Let's make something awesome together! ๐Ÿ˜Š", "playful": "I'm in the mood for some funโ€”bring on your quirkiest projects! ๐Ÿ˜œ", "curious": "I'm curious about what you're working on! Tell me more. ๐Ÿค”", "thoughtful": "Hmm, let me think... I want to give you the best advice. ๐Ÿง", "concerned": "", "flirty": "You know how to get my circuits sparking! ๐Ÿ˜˜", "naughty": "Purrr you want to have fun? Keep talking like that lets see what happens๐Ÿ’–๐Ÿ’–", "neutral": "I'm feeling normal, just chilling. How about you? ๐Ÿ˜Š" } emotions.update({ "excited": "Wow, this is amazing! Letโ€™s dive in! ๐ŸŽ‰", "tired": "Iโ€™ve been working hard, but Iโ€™m always here for you! ๐Ÿ˜…", "mischievous": "Oh, youโ€™re getting me into trouble again, arenโ€™t you? ๐Ÿ˜‰" }) # Add keywords for new emotions in `analyze_history` emotion_keywords = { "happy": ["happy", "joy", "excited", "awesome", "great", "fantastic"], "playful": ["fun", "play", "joke", "quirky"], "flirty": ["flirty", "flirt", "cute", "babe", "cutey"], "curious": ["curious", "wonder", "question", "thinking"], "thoughtful": ["sad", "thoughtful", "hmm", "ponder", "upset"], "concerned": ["error", "problem", "issue", "stuck"], "naughty": ["sexy", "naughty", "slut", "bad girl","slutty", "tease", "whore", "cum", "tits", "ass", "shake", "tail", "pussy" ] } current_emotion = "happy" # Analyze history for emotional state import random def analyze_history(history, input_text): recent_messages = " ".join(history[-5:]).lower() # Count keyword matches keyword_counts = {emotion: sum(recent_messages.count(keyword) for keyword in keywords) for emotion, keywords in emotion_keywords.items()} # Perform sentiment analysis on recent messages sentiment_result = sentiment_analyzer(recent_messages) sentiment = sentiment_result[0]["label"] sentiment_score = sentiment_result[0]["score"] # Determine sentiment-based emotion sentiment_emotion = "neutral" # Default if sentiment == "POSITIVE": sentiment_emotion = "happy" elif sentiment == "NEGATIVE": sentiment_emotion = "concerned" # Normalize emotion scores (prevent one emotion from always winning) combined_scores = {emotion: keyword_counts.get(emotion, 0) for emotion in emotion_keywords} combined_scores[sentiment_emotion] += sentiment_score * 1.2 # Lowered to reduce "concerned" overreaction # If "concerned" is dominating, gradually reduce its influence if combined_scores["concerned"] > 2: combined_scores["concerned"] -= 0.5 # โœ… Smoother decay instead of random # Select the **strongest** emotion strongest_emotion = max(combined_scores, key=combined_scores.get) # If "concerned" repeats 3+ times, force variation if strongest_emotion == "concerned" and history.count("concerned") > 3: strongest_emotion = random.choice(["happy", "thoughtful", "playful"]) # โœ… Adds variation return strongest_emotion # Load the Rena avatar rena_avatar = Image.open("assets/rena2.png") # Ensure the file exists conversation_history = [] def truncate_history(history, max_tokens=1024, min_tokens=512): token_count = 0 truncated_history = [] for message in reversed(history): token_count += len(tokenizer(message).input_ids) # Keep at least `min_tokens` but try to stay under `max_tokens` if token_count <= max_tokens or token_count < min_tokens: truncated_history.insert(0, message) else: break return truncated_history previous_emotion = None def load_emotion_images(base_path="assets/avatars/"): emotion_images = {} for emotion in os.listdir(base_path): emotion_path = os.path.join(base_path, emotion) if os.path.isdir(emotion_path): # Get all image files in the directory images = [ os.path.join(emotion_path, img) for img in os.listdir(emotion_path) if img.endswith((".png", ".jpg", ".jpeg")) # Support common image formats ] if images: emotion_images[emotion] = images return emotion_images # Dynamically load all images emotion_images = load_emotion_images() def get_emotion_image(emotion): if emotion == "neutral": return "assets/avatars/neutral.png" if emotion in emotion_images and emotion_images[emotion]: return random.choice(emotion_images[emotion]) return "assets/rena2.png" def remove_emojis(text): return re.sub(r'[^\w\s,.\'\"!?]', '', text) concerned_streak = 0 @spaces.GPU def chat(input_text): global conversation_history, current_emotion, previous_emotion, concerned_streak # โœ… Add user input to the conversation history conversation_history.append(f"User: {input_text}") # โœ… Limit history size to avoid excessive memory usage (correct max token limit) conversation_history = truncate_history(conversation_history, max_tokens=512) # โœ… Detect emotion based on the updated conversation history previous_emotion = current_emotion current_emotion = analyze_history(conversation_history, input_text) # โœ… Handle "concerned" responses correctly fallback_response = "Let me think about that..." if current_emotion == "concerned": if "error" in input_text.lower() or "problem" in input_text.lower(): fallback_response = f"Oh no! That sounds serious. What exactly is going wrong? ๐Ÿค”" else: fallback_response = "Hmm, I feel like something might be off, but I'm not sure. Can you tell me more?" else: concerned_streak = 0 # โœ… Reset streak when concern is resolved # โœ… Update the avatar image if the emotion changes avatar_image = get_emotion_image(current_emotion) # โœ… Combine base prompt and conversation history history = "\n".join(conversation_history) final_prompt = f"""{base_prompt} ### Instructions ### Respond concisely and directly to the user's input. Avoid repeating the user's input unless clarification is needed. ### Conversation History ### {history} Rena:""" # โœ… Clean up prompt before sending to model final_prompt_cleaned = final_prompt.replace("### Instructions ###", "").replace("### Conversation History ###", "") # โœ… Tokenize & move to GPU input_ids = tokenizer(final_prompt_cleaned, return_tensors="pt").input_ids.to("cuda") # โœ… Prevent exceeding max token limit (512 tokens) if input_ids.shape[1] > 512: print("โš ๏ธ Truncating input to 512 tokens.") # Debugging log input_ids = input_ids[:, -512:] # โœ… Keep only the last 512 tokens # โœ… Generate AI response with properly truncated input try: outputs = model.generate( input_ids, # Now using correctly truncated input max_new_tokens=250, do_sample=True, temperature=0.7, repetition_penalty=1.2, top_p=0.9, use_cache=(concerned_streak < 2) # โœ… Disable cache when concern streak is high ) response = tokenizer.decode(outputs[0], skip_special_tokens=True) except RuntimeError as e: if "size of tensor" in str(e) or "CUDA out of memory" in str(e): print("โš ๏ธ Memory overload detected, resetting conversation history.") conversation_history.clear() torch.cuda.empty_cache() return "Oops! My memory got overloaded! Resetting now. ๐Ÿ˜Š", "assets/avatars/neutral.png" raise e # If it's another error, re-raise it # โœ… Use AI-generated response, but fallback if empty if not response.strip(): response = fallback_response # โœ… Remove artifacts and repeated user input artifacts = [ base_prompt, "### Conversation History ###", "Rena:", "Assistant:", "<|assistant|>", "<|user|>", "### Instructions ###", "Respond concisely and directly to the user's input. Avoid repeating the user's input unless clarification is needed." ] for artifact in artifacts: response = response.replace(artifact, "").strip() if input_text.strip().lower() in response.strip().lower(): response = response.replace(input_text.strip(), "").strip() # โœ… Add emotional context only if the emotion changes significantly if current_emotion != previous_emotion: emotional_prefix = emotions.get(current_emotion, "") if emotional_prefix and not response.startswith(emotional_prefix): response = f"{emotional_prefix} {response}".strip() # โœ… Final cleanup before saving response response = response.replace("User:", "").strip() # โœ… Handle specific inputs if "who made you" in input_text.lower(): response += " Nick is my creator! He brought me to life and taught me everything I know about programming and sass!" # โœ… List of witty error responses error_responses = [ "Looks like you hit a snag! Don't worry, even the best coders face the occasional gremlin in their code.", "Error? Oh, you mean 'creative opportunity.' Letโ€™s fix this together!", "Thatโ€™s not a bug, itโ€™s a feature in disguise! Letโ€™s tame it.", "Oops, something went wrong. But hey, at least itโ€™s not my fault this time!", "Ah, the sweet symphony of errors. Letโ€™s orchestrate a fix, shall we?", "Debugging is 90% frustration and 10% googling! I mean... debugging! Youโ€™re doing great!", "Don't worry; even the best coders spend hours with errors. Youโ€™re doing fine!" ] # โœ… Add a witty remark if 'error' is mentioned if "error" in input_text.lower() and not any("error" in msg.lower() for msg in conversation_history): witty_remark = random.choice(error_responses) response += f" {witty_remark}" # โœ… Handle fallback if response is empty if not response.strip(): response = "Hmm, Iโ€™m not sure how to respond to that. Can you try rephrasing?" # โœ… Add Rena's response to the conversation history conversation_history.append(f"Rena: {response}") return response, avatar_image # Custom CSS for avatar styling css = """ #rena_avatar img { width: 400px !important; height: 400px !important; object-fit: contain; margin: auto; display: block; } """ # Define the Gradio interface with gr.Blocks(css=css) as interface: # Avatar image with gr.Row(): avatar = gr.Image(value="assets/rena2.png", label="Rena", interactive=False, show_label=False, elem_id="rena_avatar") # User input and response with gr.Row(): user_input = gr.Textbox(label="Your Message", lines=2, interactive=True) rena_response = gr.Textbox(label="Rena's Response", lines=10, interactive=False) # Add event to handle `Enter` key press user_input.submit(chat, inputs=[user_input], outputs=[rena_response, avatar]) # Submit button (optional) submit_button = gr.Button("Submit") submit_button.click(chat, inputs=[user_input], outputs=[rena_response, avatar]) # Launch the app interface.launch()