File size: 13,615 Bytes
3450372
088e989
36e292a
3dd9e9e
3450372
c93cae7
baf6658
c2c0117
c93cae7
 
 
4da75be
3450372
cce056b
 
 
844707c
0b73ff0
a75ffad
 
 
0b73ff0
a75ffad
 
8b889b9
 
 
 
a75ffad
0fcc024
0b73ff0
 
 
 
 
 
4da75be
3450372
 
7ece0f1
62fdafb
 
 
 
 
 
 
 
3450372
 
f4c1555
 
 
 
 
 
 
04ae961
36e292a
 
 
 
 
 
b4fe632
97d4ab2
 
fd27953
36e292a
64d6f7c
 
 
 
 
 
 
 
da1b74c
1d4d789
da1b74c
025f506
da1b74c
1d4d789
fd27953
f4c1555
da1b74c
64d6f7c
36e292a
 
 
fd27953
 
b4fe632
da1b74c
b4fe632
da1b74c
 
 
 
1d4d789
da1b74c
 
 
 
 
81cb660
da1b74c
 
 
81cb660
da1b74c
fd27953
da1b74c
b4fe632
da1b74c
b4fe632
 
 
fd27953
b4fe632
81cb660
1d4d789
b4fe632
fd27953
b4fe632
fd27953
81cb660
1d4d789
da1b74c
36e292a
34af5a1
fd27953
3450372
 
cb880a7
433bbe4
6f511dd
7458e39
 
6f511dd
7458e39
6f511dd
 
 
 
7458e39
 
 
6f511dd
7458e39
6f511dd
408f031
7458e39
77ae8b5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
97d4ab2
fd27953
 
f4c1555
 
 
97d4ab2
c4287b2
 
b5bfb00
e0a83aa
1da2948
3450372
0428cc0
e0a83aa
cb880a7
fba7648
cb880a7
 
b4fe632
 
5625573
fba7648
dcfb254
b4fe632
dcfb254
b4fe632
 
e0a83aa
b4fe632
 
 
 
e0a83aa
b4fe632
e0a83aa
fba7648
97d4ab2
 
fba7648
cb880a7
303665d
945fbe1
31cc637
 
303665d
31cc637
 
303665d
31cc637
f861ab7
b4fe632
 
 
 
 
02dbb76
b4fe632
02dbb76
fba7648
 
 
b4fe632
fba7648
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
cb880a7
b4fe632
 
 
 
 
945fbe1
 
 
 
 
 
 
 
 
 
7458e39
 
303665d
f861ab7
 
 
b4fe632
f861ab7
31cc637
945fbe1
e71e5b6
5d7f8f1
b4fe632
dcfb254
 
b4fe632
a888580
 
34af5a1
b4fe632
3450372
 
 
 
 
 
b4fe632
7458e39
3450372
 
b4fe632
7458e39
912259a
 
f861ab7
b4fe632
5625573
 
c93cae7
b4fe632
97d4ab2
b4fe632
c93cae7
b4fe632
0428cc0
8e75b46
12d0bd2
d50d44d
31cc637
945fbe1
dcfb254
36e292a
baf6658
 
f7d0973
 
baf6658
 
 
 
 
912259a
3450372
36e292a
edb2a81
04ae961
40042ef
edb2a81
 
04ae961
edb2a81
04ae961
f4c1555
edb2a81
c93cae7
edb2a81
 
77ae8b5
c93cae7
36e292a
aac4d82
654420b
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
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
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
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()