Commit ·
91d218d
1
Parent(s): 9872489
Deploy Run Buddy with sample chat
Browse files- app.py +174 -0
- requirements.txt +3 -0
- sample_chat.txt +100 -0
app.py
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import logging
|
| 2 |
+
import os
|
| 3 |
+
import random
|
| 4 |
+
from openai import OpenAI
|
| 5 |
+
from gtts import gTTS
|
| 6 |
+
import gradio as gr
|
| 7 |
+
|
| 8 |
+
# Configure logging
|
| 9 |
+
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
| 10 |
+
logger = logging.getLogger(__name__)
|
| 11 |
+
|
| 12 |
+
# Initialize OpenAI client with API key from environment variable
|
| 13 |
+
try:
|
| 14 |
+
os.environ["OPENAI_API_KEY"] = os.getenv("OPENAI_API_KEY")
|
| 15 |
+
if not os.environ["OPENAI_API_KEY"]:
|
| 16 |
+
raise ValueError("OPENAI_API_KEY environment variable not set!")
|
| 17 |
+
client = OpenAI()
|
| 18 |
+
except Exception as e:
|
| 19 |
+
logger.error(f"Failed to initialize OpenAI client: {e}")
|
| 20 |
+
|
| 21 |
+
# Function to process chat and generate outputs
|
| 22 |
+
def motivate_me(chat_file, name):
|
| 23 |
+
# Validate inputs
|
| 24 |
+
if not name:
|
| 25 |
+
logger.warning("No name provided")
|
| 26 |
+
return "Please enter your name!", "", "", None
|
| 27 |
+
if chat_file is None:
|
| 28 |
+
logger.warning("No chat file uploaded")
|
| 29 |
+
return "Please upload a WhatsApp chat file!", "", "", None
|
| 30 |
+
|
| 31 |
+
# Read random 500 lines from uploaded file
|
| 32 |
+
try:
|
| 33 |
+
with open(chat_file.name, "r", encoding="utf-8") as f:
|
| 34 |
+
lines = f.readlines()
|
| 35 |
+
except Exception as e:
|
| 36 |
+
logger.error(f"Error reading chat file: {e}")
|
| 37 |
+
return f"Error reading file: {str(e)}", "", "", None
|
| 38 |
+
|
| 39 |
+
if len(lines) >= 500:
|
| 40 |
+
start_index = random.randint(0, len(lines) - 500)
|
| 41 |
+
chunks = lines[start_index:start_index + 500]
|
| 42 |
+
else:
|
| 43 |
+
chunks = lines
|
| 44 |
+
logger.info(f"Selected random 500 lines, last 5: {chunks[-5:]}")
|
| 45 |
+
|
| 46 |
+
if not chunks:
|
| 47 |
+
logger.warning("No messages found in selected lines")
|
| 48 |
+
return "No messages found in the selected 500 lines!", "", "", None
|
| 49 |
+
|
| 50 |
+
messages_text = "".join(chunks)
|
| 51 |
+
|
| 52 |
+
# Summarize with OpenAI
|
| 53 |
+
summary_prompt = """Summarize the following WhatsApp conversation in 2–3 sentences, focusing only on notable events, emotional states, and meaningful interactions between 'You' ({name}) and the other user.
|
| 54 |
+
|
| 55 |
+
Highlight specific events involving {name}—especially any related to health, job, mental health, or finances.
|
| 56 |
+
|
| 57 |
+
If no major event is present, capture any noteworthy daily moments (e.g., cooking, commuting, funny moments).
|
| 58 |
+
|
| 59 |
+
Describe how {name} is feeling and what they are going through, using the format: {name} is feeling [emotion] and going through [event].
|
| 60 |
+
|
| 61 |
+
Always include the name of the other user {name} is talking to.
|
| 62 |
+
Exclude all sexual content.
|
| 63 |
+
Do not mention if "no major event" occurred—just describe what's there.
|
| 64 |
+
|
| 65 |
+
If a funny joke or banter is present, include that specific message in quotes for potential reuse.
|
| 66 |
+
|
| 67 |
+
Conversation:
|
| 68 |
+
{messages}"""
|
| 69 |
+
try:
|
| 70 |
+
response = client.chat.completions.create(
|
| 71 |
+
model="gpt-4o-mini",
|
| 72 |
+
messages=[
|
| 73 |
+
{"role": "system", "content": "You are a helpful assistant that summarizes concisely about how the user is feeling and what interactions are happening."},
|
| 74 |
+
{"role": "user", "content": summary_prompt.format(messages=messages_text, name=name)}
|
| 75 |
+
],
|
| 76 |
+
max_tokens=500,
|
| 77 |
+
temperature=0.8
|
| 78 |
+
)
|
| 79 |
+
summary = response.choices[0].message.content.strip()
|
| 80 |
+
logger.info(f"Conversation Summary: {summary}")
|
| 81 |
+
except Exception as e:
|
| 82 |
+
logger.error(f"Error generating summary: {e}")
|
| 83 |
+
return f"Error summarizing chat: {str(e)}", "", "", None
|
| 84 |
+
|
| 85 |
+
# Extract a happy/exciting interaction
|
| 86 |
+
story_prompt = """Review the following WhatsApp conversation and extract **one clear, HAPPY & exciting interaction** between participants (e.g., playful plans like "can’t wait to see you" or "let’s cook together","you got this").
|
| 87 |
+
|
| 88 |
+
- Choose only **one uplifting moment** from the chat and summarize it in 5 words about why they said it.
|
| 89 |
+
- **Exclude all sexual or suggestive content.**
|
| 90 |
+
Conversation:
|
| 91 |
+
{messages}"""
|
| 92 |
+
try:
|
| 93 |
+
response = client.chat.completions.create(
|
| 94 |
+
model="gpt-4o-mini",
|
| 95 |
+
messages=[
|
| 96 |
+
{"role": "system", "content": "You are a cute friend finding light-hearted, heartwarming, and positive messages."},
|
| 97 |
+
{"role": "user", "content": story_prompt.format(messages=messages_text)}
|
| 98 |
+
],
|
| 99 |
+
max_tokens=100,
|
| 100 |
+
temperature=0.5
|
| 101 |
+
)
|
| 102 |
+
story = response.choices[0].message.content.strip()
|
| 103 |
+
logger.info(f"Processed Story: {story}")
|
| 104 |
+
except Exception as e:
|
| 105 |
+
logger.error(f"Error generating story: {e}")
|
| 106 |
+
return summary, f"Error finding story: {str(e)}", "", None
|
| 107 |
+
|
| 108 |
+
# Generate motivation with OpenAI
|
| 109 |
+
motivation_prompt = """Using this summary: '{summary}' and this interaction: '{story}', craft a short, personal, upbeat motivational message for {name} about running.
|
| 110 |
+
|
| 111 |
+
Highlight the details from the chat and mention the user name and the moments they shared.
|
| 112 |
+
|
| 113 |
+
Emphasize how the other user brings energy, joy, and small transformations to daily life and so does running.
|
| 114 |
+
|
| 115 |
+
You could also reference the included message exactly as it appears in the summary or story—remind them of it while tying it to running, if you wish to. Only add that message if it has meaning on its own. Dont add a single worded message.
|
| 116 |
+
Think about that message and then connect it with the situation in which they said it and top it up with a nice motivational quote!
|
| 117 |
+
|
| 118 |
+
Connect everything into something sweet, and encouraging.
|
| 119 |
+
|
| 120 |
+
If the summary has sad/worrisome emotion - you can use something soothing to acknowledge the hurt. Example: "It’s okay to feel heavy. Let your feet be light for now—run it off, breathe, come back brighter. ({name})" Come up with your own.
|
| 121 |
+
|
| 122 |
+
Keep in mind, all these incidents happened in the PAST. You are just reminding the user of their strength/fun loving nature.
|
| 123 |
+
Word limit: 50 words
|
| 124 |
+
Tone: Positive, personal, and warm
|
| 125 |
+
Must do: Mention the name of the other user in this motivational message.
|
| 126 |
+
End with: ({name})"""
|
| 127 |
+
try:
|
| 128 |
+
response = client.chat.completions.create(
|
| 129 |
+
model="gpt-4o-mini",
|
| 130 |
+
messages=[
|
| 131 |
+
{"role": "system", "content": "You are a motivational coach using personal events and stories from a user's life."},
|
| 132 |
+
{"role": "user", "content": motivation_prompt.format(summary=summary, story=story, name=name)}
|
| 133 |
+
],
|
| 134 |
+
max_tokens=100,
|
| 135 |
+
temperature=0.7
|
| 136 |
+
)
|
| 137 |
+
motivation = response.choices[0].message.content.strip()
|
| 138 |
+
logger.info(f"Motivation generated: {motivation}")
|
| 139 |
+
except Exception as e:
|
| 140 |
+
logger.error(f"Error generating motivation: {e}")
|
| 141 |
+
return summary, story, f"Error generating motivation: {str(e)}", None
|
| 142 |
+
|
| 143 |
+
# Save and return audio
|
| 144 |
+
try:
|
| 145 |
+
tts = gTTS(motivation, lang="en")
|
| 146 |
+
audio_file = "motivation.mp3"
|
| 147 |
+
tts.save(audio_file)
|
| 148 |
+
logger.info("Audio file generated successfully")
|
| 149 |
+
except Exception as e:
|
| 150 |
+
logger.error(f"Error generating audio: {e}")
|
| 151 |
+
return summary, story, motivation, None
|
| 152 |
+
|
| 153 |
+
return summary, story, motivation, audio_file
|
| 154 |
+
|
| 155 |
+
# Gradio interface
|
| 156 |
+
interface = gr.Interface(
|
| 157 |
+
fn=motivate_me,
|
| 158 |
+
inputs=[
|
| 159 |
+
gr.File(label="Upload WhatsApp Chat (_chat.txt)"),
|
| 160 |
+
gr.Textbox(label="Your Name", value="", placeholder="Enter your name (e.g., Alex)")
|
| 161 |
+
],
|
| 162 |
+
outputs=[
|
| 163 |
+
gr.Textbox(label="Conversation Summary"),
|
| 164 |
+
gr.Textbox(label="Wholesome Interaction"),
|
| 165 |
+
gr.Textbox(label="Your Motivation"),
|
| 166 |
+
gr.Audio(label="Listen Up!")
|
| 167 |
+
],
|
| 168 |
+
title="Run Buddy: Your Personal Motivator",
|
| 169 |
+
description="Upload your WhatsApp chat (_chat.txt) and enter your name to get a summary, a happy moment, and a running pep talk from 500 random messages! Check out the sample file [here](https://huggingface.co/spaces/your-username/RunBuddyMotivator/raw/main/sample_chat.txt).",
|
| 170 |
+
theme="soft"
|
| 171 |
+
)
|
| 172 |
+
|
| 173 |
+
if __name__ == "__main__":
|
| 174 |
+
interface.launch()
|
requirements.txt
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
openai
|
| 2 |
+
gtts
|
| 3 |
+
gradio
|
sample_chat.txt
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[03/01/25, 09:00:00] Alex: Morning! How’s my favorite human?
|
| 2 |
+
[03/01/25, 09:01:10] Jamie: Tired. Coffee machine broke again.
|
| 3 |
+
[03/01/25, 09:02:20] Alex: You’re a zombie without coffee!
|
| 4 |
+
[03/01/25, 09:03:30] Jamie: True. Shuffling to the store now.
|
| 5 |
+
[03/01/25, 09:04:40] Alex: Don’t trip over your slippers!
|
| 6 |
+
[03/01/25, 09:05:50] Jamie: Haha, "fell into a bush once!"
|
| 7 |
+
[03/01/25, 09:06:00] Alex: Legendary. Bush still mad?
|
| 8 |
+
[03/01/25, 09:07:10] Jamie: Probably plotting revenge.
|
| 9 |
+
[03/01/25, 09:08:20] Alex: Watch out for ninja leaves!
|
| 10 |
+
[03/01/25, 09:09:30] Jamie: I’ll wear armor next time!
|
| 11 |
+
[03/01/25, 10:00:00] Alex: Work today?
|
| 12 |
+
[03/01/25, 10:01:15] Jamie: Nope. Got laid off yesterday.
|
| 13 |
+
[03/01/25, 10:02:25] Alex: What?! Are you okay?
|
| 14 |
+
[03/01/25, 10:03:35] Jamie: Shocked. Feeling lost honestly.
|
| 15 |
+
[03/01/25, 10:04:45] Alex: That sucks. Bosses are idiots.
|
| 16 |
+
[03/01/25, 10:05:55] Jamie: Yeah, "budget cuts" they said.
|
| 17 |
+
[03/01/25, 10:06:05] Alex: Budget cuts my foot!
|
| 18 |
+
[03/01/25, 10:07:15] Jamie: Now I’m jobless and coffeeless.
|
| 19 |
+
[03/01/25, 10:08:25] Alex: We’ll fix that. Coffee first?
|
| 20 |
+
[03/01/25, 10:09:35] Jamie: Please. Meet at the café?
|
| 21 |
+
[03/01/25, 11:00:00] Alex: Just spilled coffee on my shirt.
|
| 22 |
+
[03/01/25, 11:01:10] Jamie: Haha, "you’re a walking disaster!"
|
| 23 |
+
[03/01/25, 11:02:20] Alex: Only when you’re around!
|
| 24 |
+
[03/01/25, 11:03:30] Jamie: Blaming me for your clumsiness?
|
| 25 |
+
[03/01/25, 11:04:40] Alex: Yup, you’re my chaos magnet.
|
| 26 |
+
[03/01/25, 11:05:50] Jamie: Fair. I attract chaos naturally.
|
| 27 |
+
[03/01/25, 12:00:00] Alex: Lunch plans?
|
| 28 |
+
[03/01/25, 12:01:15] Jamie: Cooking pasta. Wanna join?
|
| 29 |
+
[03/01/25, 12:02:25] Alex: Only if we don’t burn it!
|
| 30 |
+
[03/01/25, 12:03:35] Jamie: "Last time was epic flames!"
|
| 31 |
+
[03/01/25, 12:04:45] Alex: Fire alarm’s still traumatized.
|
| 32 |
+
[03/01/25, 12:05:55] Jamie: We’ll be careful this time.
|
| 33 |
+
[03/01/25, 12:06:05] Alex: Famous last words!
|
| 34 |
+
[03/01/25, 13:00:00] Jamie: Job hunt starts tomorrow.
|
| 35 |
+
[03/01/25, 13:01:10] Alex: You got this, Jamie!
|
| 36 |
+
[03/01/25, 13:02:20] Jamie: Hope so. Feeling down still.
|
| 37 |
+
[03/01/25, 13:03:30] Alex: Let’s distract you tonight.
|
| 38 |
+
[03/01/25, 13:04:40] Jamie: Movie night?
|
| 39 |
+
[03/01/25, 13:05:50] Alex: Yup! Popcorn’s on me.
|
| 40 |
+
[03/01/25, 14:00:00] Jamie: Just tripped over my cat.
|
| 41 |
+
[03/01/25, 14:01:10] Alex: Cat okay? You okay?
|
| 42 |
+
[03/01/25, 14:02:20] Jamie: Cat glared. I’m bruised.
|
| 43 |
+
[03/01/25, 14:03:30] Alex: "Cat’s plotting world domination now!"
|
| 44 |
+
[03/01/25, 14:04:40] Jamie: Probably. She’s the boss here.
|
| 45 |
+
[03/01/25, 14:05:50] Alex: Bow to Queen Cat!
|
| 46 |
+
[03/01/25, 15:00:00] Jamie: Applied to three jobs.
|
| 47 |
+
[03/01/25, 15:01:10] Alex: Nice! Fingers crossed.
|
| 48 |
+
[03/01/25, 15:02:20] Jamie: Thanks. Waiting’s the worst.
|
| 49 |
+
[03/01/25, 15:03:30] Alex: Keep busy—run with me later?
|
| 50 |
+
[03/01/25, 15:04:40] Jamie: Maybe. Legs feel like jelly.
|
| 51 |
+
[03/01/25, 16:00:00] Alex: Bought groceries for dinner.
|
| 52 |
+
[03/01/25, 16:01:10] Jamie: Sweet! What’s on the menu?
|
| 53 |
+
[03/01/25, 16:02:20] Alex: Tacos. No fires this time!
|
| 54 |
+
[03/01/25, 16:03:30] Jamie: "Taco Tuesday flashback—spicy chaos!"
|
| 55 |
+
[03/01/25, 16:04:40] Alex: We survived the salsa explosion!
|
| 56 |
+
[03/01/25, 16:05:50] Jamie: Barely. Kitchen smelled for days.
|
| 57 |
+
[03/01/25, 17:00:00] Jamie: Cat stole a taco shell.
|
| 58 |
+
[03/01/25, 17:01:10] Alex: Haha, "cat’s a taco bandit!"
|
| 59 |
+
[03/01/25, 17:02:20] Jamie: She crunched it loudly.
|
| 60 |
+
[03/01/25, 17:03:30] Alex: Give her a chef hat!
|
| 61 |
+
[03/01/25, 17:04:40] Jamie: She’d wear it proudly.
|
| 62 |
+
[03/01/25, 18:00:00] Alex: How’s the jobless vibe now?
|
| 63 |
+
[03/01/25, 18:01:10] Jamie: Still weird. Less panicked though.
|
| 64 |
+
[03/01/25, 18:02:20] Alex: Progress! You’re tougher than this.
|
| 65 |
+
[03/01/25, 18:03:30] Jamie: Thanks, Alex. Means a lot.
|
| 66 |
+
[03/01/25, 19:00:00] Jamie: Watched a funny cat video.
|
| 67 |
+
[03/01/25, 19:01:10] Alex: Send it over!
|
| 68 |
+
[03/01/25, 19:02:20] Jamie: "Cat vs. cucumber—total freakout!"
|
| 69 |
+
[03/01/25, 19:03:30] Alex: Dying laughing over here!
|
| 70 |
+
[03/01/25, 19:04:40] Jamie: Glad I could help!
|
| 71 |
+
[03/01/25, 20:00:00] Alex: Movie time soon?
|
| 72 |
+
[03/01/25, 20:01:10] Jamie: Yup, couch is ready.
|
| 73 |
+
[03/01/25, 20:02:20] Alex: Bringing extra popcorn!
|
| 74 |
+
[03/01/25, 20:03:30] Jamie: "Last time we spilled everywhere!"
|
| 75 |
+
[03/01/25, 20:04:40] Alex: Cat probably ate it all.
|
| 76 |
+
[03/01/25, 21:00:00] Jamie: Movie was hilarious.
|
| 77 |
+
[03/01/25, 21:01:10] Alex: That plot twist though!
|
| 78 |
+
[03/01/25, 21:02:20] Jamie: Didn’t see it coming.
|
| 79 |
+
[03/01/25, 21:03:30] Alex: Next time, horror?
|
| 80 |
+
[03/01/25, 21:04:40] Jamie: Sure, if I don’t scream!
|
| 81 |
+
[03/01/25, 22:00:00] Jamie: Bedtime. Exhausted today.
|
| 82 |
+
[03/01/25, 22:01:10] Alex: Sleep well, jobless warrior!
|
| 83 |
+
[03/01/25, 22:02:20] Jamie: Haha, thanks. Night!
|
| 84 |
+
[03/01/25, 22:03:30] Alex: Night! Dream of tacos.
|
| 85 |
+
[03/02/25, 08:00:00] Jamie: Up early. Job hunt again.
|
| 86 |
+
[03/02/25, 08:01:10] Alex: You’re unstoppable!
|
| 87 |
+
[03/02/25, 08:02:20] Jamie: Trying to be. Coffee’s brewing.
|
| 88 |
+
[03/02/25, 08:03:30] Alex: Fixed the machine?
|
| 89 |
+
[03/02/25, 08:04:40] Jamie: Yup, "hammered it into submission!"
|
| 90 |
+
[03/02/25, 08:05:50] Alex: Coffee machine fears you now!
|
| 91 |
+
[03/02/25, 09:00:00] Jamie: Resume sent to five places.
|
| 92 |
+
[03/02/25, 09:01:10] Alex: Awesome! They’ll beg for you.
|
| 93 |
+
[03/02/25, 09:02:20] Jamie: Hope so. Fingers crossed.
|
| 94 |
+
[03/02/25, 10:00:00] Alex: Run today? Clear your head?
|
| 95 |
+
[03/02/25, 10:01:10] Jamie: Maybe. Feeling a bit lighter.
|
| 96 |
+
[03/02/25, 10:02:20] Alex: That’s the spirit!
|
| 97 |
+
[03/02/25, 11:00:00] Jamie: Cat’s napping on my laptop.
|
| 98 |
+
[03/02/25, 11:01:10] Alex: "She’s the real boss now!"
|
| 99 |
+
[03/02/25, 11:02:20] Jamie: Always has been!
|
| 100 |
+
[03/02/25, 11:03:30] Alex: Pet her for luck!
|