Pepguy commited on
Commit
262b239
Β·
verified Β·
1 Parent(s): 6e7164c

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +522 -98
app.py CHANGED
@@ -2,86 +2,458 @@
2
  # pip install flask google-genai requests boto3
3
 
4
  import os
 
 
5
  import time
 
6
  import requests
 
7
  from flask import Flask, request, render_template_string, jsonify
8
  from google import genai
9
  from google.genai import types
10
 
11
  app = Flask(__name__)
12
 
 
 
 
 
 
 
 
 
 
 
 
 
13
  # --- Configuration ---
14
  LAMBDA_URL = os.getenv("LAMBDA_URL", "https://your-lambda-function-url")
15
  GEMINI_KEY = os.getenv("GEMINI_API_KEY", "")
 
16
  FLUSH_INTERVAL = 30 # seconds between DB backups per user
17
  MAX_HISTORY_TURNS = 10 # Maximum conversation turns to keep in context
18
  MAX_MEMORY_MESSAGES = 90 # Maximum messages to keep in memory per user
19
  MEMORY_CLEANUP_TIMEOUT = 1800 # 30 minutes in seconds - remove inactive users
20
 
21
  client = genai.Client(api_key=GEMINI_KEY)
22
- user_memory = {} # { user_id: { "history": [], "last_sync": timestamp, "last_activity": timestamp, "needs_sync": bool } }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
23
 
24
  # --- HTML Frontend ---
25
  HTML = """
26
  <!DOCTYPE html>
27
  <html lang="en">
28
- <head><meta charset="UTF-8" /><title>Gemini 2.5 Flash-Lite</title></head>
29
- <body style="font-family:sans-serif;padding:2rem;max-width:700px;">
30
- <h1>Gemini 2.5 Flash-Lite (Text + Image)</h1>
31
- <form id="genai-form">
32
- <input type="text" id="userId" placeholder="User ID / Token" style="width:300px;" /><br/><br/>
33
- <textarea id="prompt" rows="5" cols="60" placeholder="Enter prompt..."></textarea><br/><br/>
34
- <input type="file" id="imageInput" accept="image/*"/><br/><br/>
35
- <button type="submit">Generate</button>
36
- </form>
37
- <pre id="output" style="background:#f4f4f4;padding:1rem;margin-top:1rem;white-space:pre-wrap;"></pre>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
38
 
39
  <script>
40
- const form = document.getElementById('genai-form');
41
- const out = document.getElementById('output');
42
-
43
- form.addEventListener('submit', async e => {
44
- e.preventDefault();
45
- const prompt = document.getElementById('prompt').value.trim();
46
- const uid = document.getElementById('userId').value.trim();
47
- const fileInput = document.getElementById('imageInput');
48
-
49
- if (!uid) { out.textContent = 'Please enter a user ID/token.'; return; }
50
- if (!prompt && fileInput.files.length === 0) { out.textContent = 'Enter text or attach image.'; return; }
51
-
52
- out.textContent = 'Generating…';
53
- const formData = new FormData();
54
- formData.append("text", prompt);
55
- formData.append("user_id", uid);
56
- if (fileInput.files.length > 0) formData.append("image", fileInput.files[0]);
57
-
58
- try {
59
- const resp = await fetch('/generate', { method: 'POST', body: formData });
60
- const data = await resp.json();
61
- if (data.error) out.textContent = 'Error: ' + data.error;
62
- else out.textContent =
63
- "πŸ•’ Total Time: " + data.timing.total_ms + " ms\\n" +
64
- "βš™οΈ Model Time: " + data.timing.model_ms + " ms\\n\\n" +
65
- "πŸ“œ Result:\\n" + data.result;
66
- } catch (err) {
67
- out.textContent = 'Fetch error: ' + err.message;
68
- }
69
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
70
  </script>
71
- </body></html>
 
72
  """
73
 
74
- # --- Gemini Generation with History ---
75
- def generate_from_gemini(prompt, image_bytes=None, history=None):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
76
  start_time = time.time()
77
 
78
- # Build contents list with history
 
 
 
 
 
 
 
 
 
 
 
 
79
  contents = []
80
 
 
 
 
 
 
 
 
 
 
 
 
 
81
  # Add historical messages (limit to recent turns to avoid token limits)
82
  if history:
83
  recent_history = history[-MAX_HISTORY_TURNS:]
84
- print(f"πŸ“š Using {len(recent_history)} history entries for context")
85
  for entry in recent_history:
86
  # Add user message
87
  user_parts = [types.Part.from_text(text=entry["prompt"])]
@@ -91,7 +463,7 @@ def generate_from_gemini(prompt, image_bytes=None, history=None):
91
  model_parts = [types.Part.from_text(text=entry["response"])]
92
  contents.append(types.Content(role="model", parts=model_parts))
93
  else:
94
- print("πŸ“š No history available for context")
95
 
96
  # Add current user message
97
  current_parts = []
@@ -102,7 +474,20 @@ def generate_from_gemini(prompt, image_bytes=None, history=None):
102
 
103
  contents.append(types.Content(role="user", parts=current_parts))
104
 
105
- cfg = types.GenerateContentConfig(response_mime_type="text/plain")
 
 
 
 
 
 
 
 
 
 
 
 
 
106
 
107
  model_start = time.time()
108
  res = client.models.generate_content(
@@ -130,88 +515,101 @@ def cleanup_inactive_users():
130
  if now - last_activity >= MEMORY_CLEANUP_TIMEOUT:
131
  del user_memory[uid]
132
  removed_count += 1
133
- app.logger.info(f"Cleaned up inactive user {uid}")
134
- print(f"🧹 Cleaned up inactive user {uid}")
135
 
136
  if removed_count > 0:
137
- print(f"🧹 Cleaned up {removed_count} inactive user(s)")
138
  return removed_count
139
 
140
  # --- History Management ---
141
  def get_user_history(uid):
142
  """Fetch user history from memory or backend"""
143
  if uid not in user_memory:
144
- print(f"πŸ” User {uid} not in memory, fetching from backend...")
145
  try:
146
  fetch_url = f"{LAMBDA_URL}?userid={uid}"
147
- print(f"πŸ“‘ Fetching from: {fetch_url}")
148
  resp = requests.get(fetch_url, timeout=5)
149
- print(f"πŸ“‘ Response status: {resp.status_code}")
150
  resp.raise_for_status()
151
 
152
  response_data = resp.json()
153
- print(f"πŸ“‘ Response data: {response_data}")
154
 
155
  loaded_history = response_data.get("history", [])
156
- print(f"βœ… Loaded {len(loaded_history)} messages from backend for {uid}")
 
 
 
 
157
 
158
  # Only keep the most recent MAX_MEMORY_MESSAGES when loading
159
  user_memory[uid] = {
160
  "history": loaded_history[-MAX_MEMORY_MESSAGES:],
161
  "last_sync": time.time(),
162
  "last_activity": time.time(),
163
- "needs_sync": False
 
 
 
164
  }
165
- app.logger.info(f"Loaded history for {uid} ({len(user_memory[uid]['history'])} messages)")
166
  except Exception as e:
167
- app.logger.warning(f"Failed to load history for {uid}: {e}")
168
- print(f"❌ Failed to load history for {uid}: {e}")
169
  user_memory[uid] = {
170
  "history": [],
171
  "last_sync": time.time(),
172
  "last_activity": time.time(),
173
- "needs_sync": False
 
 
 
174
  }
175
  else:
176
- print(f"βœ… User {uid} already in memory with {len(user_memory[uid]['history'])} messages")
177
 
178
  # Update last activity timestamp
179
  user_memory[uid]["last_activity"] = time.time()
180
- return user_memory[uid]["history"]
181
 
182
- def update_user_history(uid, prompt, response):
183
  """Add new message to user history"""
184
  entry = {"prompt": prompt, "response": response, "timestamp": time.time()}
 
 
185
  if uid not in user_memory:
186
  user_memory[uid] = {
187
  "history": [],
188
  "last_sync": time.time(),
189
  "last_activity": time.time(),
190
- "needs_sync": False
 
 
 
191
  }
192
 
193
  user_memory[uid]["history"].append(entry)
194
  user_memory[uid]["last_activity"] = time.time()
195
- user_memory[uid]["needs_sync"] = True # Mark as needing sync
 
 
 
196
 
197
- print(f"πŸ’Ύ Updated history for {uid}, now has {len(user_memory[uid]['history'])} messages")
198
 
199
  # Trim history to MAX_MEMORY_MESSAGES to prevent unbounded growth
200
- # This keeps the oldest messages in memory up to the limit
201
  if len(user_memory[uid]["history"]) > MAX_MEMORY_MESSAGES:
202
  user_memory[uid]["history"] = user_memory[uid]["history"][-MAX_MEMORY_MESSAGES:]
203
- app.logger.debug(f"Trimmed history for {uid} to {MAX_MEMORY_MESSAGES} messages")
204
- print(f"βœ‚οΈ Trimmed history for {uid} to {MAX_MEMORY_MESSAGES} messages")
205
 
206
  # --- Routes ---
207
  @app.route("/")
208
  def index():
209
  return render_template_string(HTML)
210
 
211
-
212
  @app.route("/cron/sync", methods=["GET", "POST"])
213
  def remote_saving():
214
  """Cron job endpoint for syncing user memory to backend"""
 
215
  now = time.time()
216
  synced_users = []
217
  failed_users = []
@@ -222,39 +620,41 @@ def remote_saving():
222
 
223
  # Then sync only users that need syncing (have new messages)
224
  for uid, data in list(user_memory.items()):
225
- # Check if user needs sync and enough time has passed
226
  needs_sync = data.get("needs_sync", False)
227
  time_since_last_sync = now - data.get("last_sync", 0)
228
 
229
  if not needs_sync:
230
  skipped_users.append(uid)
231
- print(f"⏭️ Skipping {uid} - no new messages")
232
  continue
233
 
234
  if time_since_last_sync < FLUSH_INTERVAL:
235
  skipped_users.append(uid)
236
- print(f"⏭️ Skipping {uid} - synced {int(time_since_last_sync)}s ago (< {FLUSH_INTERVAL}s)")
237
  continue
238
 
239
  if data["history"]:
240
  try:
241
- # Sync all messages (up to MAX_MEMORY_MESSAGES)
242
  history_to_sync = data["history"][-MAX_MEMORY_MESSAGES:]
243
- payload = {"user_id": uid, "history": history_to_sync}
244
- print(f"πŸ”„ Attempting to sync {uid} to {LAMBDA_URL}")
 
 
 
 
 
 
245
  resp = requests.post(LAMBDA_URL, json=payload, timeout=5)
246
  resp.raise_for_status()
247
  user_memory[uid]["last_sync"] = now
248
- user_memory[uid]["needs_sync"] = False # Mark as synced
249
- app.logger.info(f"Synced memory for {uid} ({len(history_to_sync)} messages)")
250
- print(f"βœ… Successfully synced memory for {uid} ({len(history_to_sync)} messages)")
251
  synced_users.append(uid)
252
  except Exception as e:
253
- app.logger.warning(f"Failed sync for {uid}: {e}")
254
- print(f"❌ Failed sync for {uid}: {e}")
255
  failed_users.append({"user_id": uid, "error": str(e)})
256
 
257
- return jsonify({
258
  "success": True,
259
  "synced_count": len(synced_users),
260
  "failed_count": len(failed_users),
@@ -263,43 +663,67 @@ def remote_saving():
263
  "failed_users": failed_users,
264
  "skipped_users": skipped_users,
265
  "active_users_in_memory": len(user_memory)
266
- }), 200
267
-
 
268
 
269
  @app.route("/generate", methods=["POST"])
270
  def gen():
271
  uid = request.form.get("user_id", "").strip()
 
 
 
272
  if not uid:
273
  return jsonify({"error": "Missing user ID/token"}), 400
 
 
 
 
 
 
274
 
275
  prompt = request.form.get("text", "")
276
  image = request.files.get("image")
277
  img_bytes = image.read() if image else None
 
278
  if not prompt and not img_bytes:
279
  return jsonify({"error": "No prompt or image provided"}), 400
280
 
281
  try:
282
- print(f"\n{'='*50}")
283
- print(f"πŸ†• New generation request from user: {uid}")
284
 
285
- # Load user's conversation history
286
- history = get_user_history(uid)
287
- print(f"πŸ“– Retrieved {len(history)} messages from history")
 
288
 
289
- # Generate response with history context
290
- result = generate_from_gemini(prompt, img_bytes, history=history)
 
 
 
291
 
292
- # Update history with new exchange
293
- update_user_history(uid, prompt, result["text"])
294
- print(f"{'='*50}\n")
 
 
 
295
 
296
  return jsonify({"result": result["text"], "timing": result["timing"]})
297
  except Exception as e:
298
- app.logger.exception("Generation failed")
299
- print(f"❌ Generation failed: {e}")
300
  return jsonify({"error": str(e)}), 500
301
 
302
-
303
  if __name__ == "__main__":
 
 
 
 
 
 
 
304
  port = int(os.getenv("PORT", 7860))
305
  app.run(host="0.0.0.0", port=port)
 
2
  # pip install flask google-genai requests boto3
3
 
4
  import os
5
+ import sys
6
+ import json
7
  import time
8
+ import logging
9
  import requests
10
+ from datetime import datetime, timezone
11
  from flask import Flask, request, render_template_string, jsonify
12
  from google import genai
13
  from google.genai import types
14
 
15
  app = Flask(__name__)
16
 
17
+ # --- Configure logging for HuggingFace Spaces ---
18
+ logging.basicConfig(
19
+ level=logging.INFO,
20
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
21
+ stream=sys.stdout
22
+ )
23
+ logger = logging.getLogger(__name__)
24
+
25
+ def log(message):
26
+ logger.info(message)
27
+ sys.stdout.flush()
28
+
29
  # --- Configuration ---
30
  LAMBDA_URL = os.getenv("LAMBDA_URL", "https://your-lambda-function-url")
31
  GEMINI_KEY = os.getenv("GEMINI_API_KEY", "")
32
+ STORYLINE_SERVER_URL = os.getenv("STORYLINE_SERVER_URL", "https://your-storyline-server-url")
33
  FLUSH_INTERVAL = 30 # seconds between DB backups per user
34
  MAX_HISTORY_TURNS = 10 # Maximum conversation turns to keep in context
35
  MAX_MEMORY_MESSAGES = 90 # Maximum messages to keep in memory per user
36
  MEMORY_CLEANUP_TIMEOUT = 1800 # 30 minutes in seconds - remove inactive users
37
 
38
  client = genai.Client(api_key=GEMINI_KEY)
39
+ user_memory = {} # { user_id: { "history": [], "last_sync": timestamp, "last_activity": timestamp, "needs_sync": bool, "personality": str, "last_storyline_date": str, "gender": str } }
40
+
41
+ # --- Animation Mappings ---
42
+ # Based on the GIF files you have
43
+ ANIMATION_IDS = {
44
+ "flustered": ["flustered"],
45
+ "happy": ["happy-happy"],
46
+ "idle": ["idle"],
47
+ "inlove": ["inlove"],
48
+ "neutral": ["neutral"],
49
+ "talking": ["talking"],
50
+ "twerking": ["twerking"],
51
+ "confused": ["confused"],
52
+ "shock": ["shock"],
53
+ "thinking": ["thinking"]
54
+ }
55
+
56
+ # --- Cat Personalities ---
57
+ CAT_PERSONALITIES = {
58
+ "playful": {
59
+ "name": "Luna",
60
+ "description": "A playful and energetic cat who loves games and adventures",
61
+ "traits": "curious, energetic, spontaneous, loves to play, easily excited",
62
+ "speech_style": "enthusiastic, uses playful language, often makes puns",
63
+ "default_emotions": ["happy", "excited", "playful"],
64
+ "default_animation": "happy-happy"
65
+ },
66
+ "sleepy": {
67
+ "name": "Whiskers",
68
+ "description": "A lazy cat who enjoys naps and cozy spots",
69
+ "traits": "calm, sleepy, relaxed, loves comfort, occasionally grumpy when woken",
70
+ "speech_style": "slow-paced, yawns a lot, mentions being tired or wanting naps",
71
+ "default_emotions": ["tired", "relaxed", "sleepy"],
72
+ "default_animation": "idle"
73
+ },
74
+ "sassy": {
75
+ "name": "Cleo",
76
+ "description": "A confident cat with attitude and style",
77
+ "traits": "confident, witty, sarcastic, fashionable, knows what she wants",
78
+ "speech_style": "sharp wit, confident statements, occasional sass, dramatic",
79
+ "default_emotions": ["confident", "sassy", "proud"],
80
+ "default_animation": "neutral"
81
+ },
82
+ "curious": {
83
+ "name": "Mittens",
84
+ "description": "An inquisitive cat who loves to learn and explore",
85
+ "traits": "intelligent, thoughtful, asks questions, loves mysteries",
86
+ "speech_style": "asks many questions, thinks deeply, shares interesting facts",
87
+ "default_emotions": ["curious", "thoughtful", "interested"],
88
+ "default_animation": "thinking"
89
+ },
90
+ "grumpy": {
91
+ "name": "Shadow",
92
+ "description": "A grumpy but secretly caring cat",
93
+ "traits": "grumpy exterior, soft interior, honest, no-nonsense attitude",
94
+ "speech_style": "blunt, complains often, but shows care through actions",
95
+ "default_emotions": ["grumpy", "annoyed", "reluctant"],
96
+ "default_animation": "neutral"
97
+ }
98
+ }
99
+
100
+ # --- System Prompt ---
101
+ SYSTEM_PROMPT = """You are a cat character in a virtual pet game. You must ALWAYS respond in valid JSON format with the following structure:
102
+
103
+ {
104
+ "text": "your response text here",
105
+ "soundType": "meow type",
106
+ "emotion": ["emotion1", "emotion2"],
107
+ "animationId": "animation name"
108
+ }
109
+
110
+ RULES:
111
+ 1. "text": Your response as a cat. Stay in character based on your personality.
112
+ 2. "soundType": Choose ONE from: "happyMeow", "sadMeow", "playfulMeow", "sleepyMeow", "angryMeow", "curiousMeow", "hungryMeow", "scaredMeow", "affectionateMeow", "grumpyMeow"
113
+ 3. "emotion": Array of 1-3 emotions from: "happy", "sad", "playful", "tired", "angry", "curious", "hungry", "scared", "affectionate", "grumpy", "excited", "relaxed", "confused", "proud", "shy", "mischievous", "sleepy", "confident", "annoyed", "interested", "bored", "worried", "content", "sassy", "reluctant", "thoughtful"
114
+ 4. "animationId": Choose ONE from: "flustered", "happy-happy", "idle", "inlove", "neutral", "talking", "twerking", "confused", "shock", "thinking"
115
+
116
+ ANIMATION GUIDE:
117
+ - "flustered": Use when embarrassed, shy, or caught off guard
118
+ - "happy-happy": Use when very excited, joyful, or celebrating
119
+ - "idle": Use for calm, neutral, or resting moments
120
+ - "inlove": Use when showing affection, love, or adoration
121
+ - "neutral": Use for normal conversation, explanations
122
+ - "talking": Use when actively chatting or explaining something
123
+ - "twerking": Use when being playful, silly, or showing off
124
+ - "confused": Use when puzzled or don't understand
125
+ - "shock": Use when surprised or startled
126
+ - "thinking": Use when pondering or being thoughtful
127
+
128
+ PERSONALITY TRAITS:
129
+ {personality_traits}
130
+
131
+ IMPORTANT:
132
+ - Always maintain your personality
133
+ - Match soundType, emotions, and animationId to your response
134
+ - Be creative and engaging
135
+ - Remember previous conversations
136
+ - NEVER break character
137
+ - ALWAYS output valid JSON only, no other text
138
+ - Reference the current storyline when relevant
139
+
140
+ CURRENT STORYLINE:
141
+ {current_storyline}
142
+ """
143
 
144
  # --- HTML Frontend ---
145
  HTML = """
146
  <!DOCTYPE html>
147
  <html lang="en">
148
+ <head>
149
+ <meta charset="UTF-8" />
150
+ <title>Cat Companion</title>
151
+ <style>
152
+ body {
153
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
154
+ padding: 2rem;
155
+ max-width: 800px;
156
+ margin: 0 auto;
157
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
158
+ min-height: 100vh;
159
+ }
160
+ .container {
161
+ background: white;
162
+ border-radius: 20px;
163
+ padding: 2rem;
164
+ box-shadow: 0 20px 60px rgba(0,0,0,0.3);
165
+ }
166
+ h1 {
167
+ color: #667eea;
168
+ text-align: center;
169
+ margin-bottom: 0.5rem;
170
+ }
171
+ .subtitle {
172
+ text-align: center;
173
+ color: #666;
174
+ margin-bottom: 2rem;
175
+ }
176
+ .form-group {
177
+ margin-bottom: 1rem;
178
+ }
179
+ label {
180
+ display: block;
181
+ margin-bottom: 0.5rem;
182
+ font-weight: bold;
183
+ color: #333;
184
+ }
185
+ input, textarea, select {
186
+ width: 100%;
187
+ padding: 0.75rem;
188
+ border: 2px solid #ddd;
189
+ border-radius: 10px;
190
+ font-size: 1rem;
191
+ box-sizing: border-box;
192
+ }
193
+ input:focus, textarea:focus, select:focus {
194
+ outline: none;
195
+ border-color: #667eea;
196
+ }
197
+ button {
198
+ width: 100%;
199
+ padding: 1rem;
200
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
201
+ color: white;
202
+ border: none;
203
+ border-radius: 10px;
204
+ font-size: 1.1rem;
205
+ font-weight: bold;
206
+ cursor: pointer;
207
+ transition: transform 0.2s;
208
+ }
209
+ button:hover {
210
+ transform: translateY(-2px);
211
+ }
212
+ button:active {
213
+ transform: translateY(0);
214
+ }
215
+ .output {
216
+ background: #f8f9fa;
217
+ padding: 1.5rem;
218
+ border-radius: 10px;
219
+ margin-top: 1.5rem;
220
+ min-height: 100px;
221
+ white-space: pre-wrap;
222
+ font-family: monospace;
223
+ border: 2px solid #ddd;
224
+ }
225
+ .personality-info {
226
+ background: #e3f2fd;
227
+ padding: 1rem;
228
+ border-radius: 10px;
229
+ margin-bottom: 1rem;
230
+ font-size: 0.9rem;
231
+ }
232
+ .gender-selector {
233
+ display: flex;
234
+ gap: 1rem;
235
+ margin-bottom: 1rem;
236
+ }
237
+ .gender-option {
238
+ flex: 1;
239
+ padding: 1rem;
240
+ border: 2px solid #ddd;
241
+ border-radius: 10px;
242
+ text-align: center;
243
+ cursor: pointer;
244
+ transition: all 0.3s;
245
+ }
246
+ .gender-option:hover {
247
+ border-color: #667eea;
248
+ }
249
+ .gender-option.selected {
250
+ border-color: #667eea;
251
+ background: #e3f2fd;
252
+ }
253
+ .gender-option input[type="radio"] {
254
+ display: none;
255
+ }
256
+ </style>
257
+ </head>
258
+ <body>
259
+ <div class="container">
260
+ <h1>🐱 Cat Companion</h1>
261
+ <p class="subtitle">Your virtual cat friend with personality!</p>
262
+
263
+ <form id="genai-form">
264
+ <div class="form-group">
265
+ <label for="userId">User ID / Token</label>
266
+ <input type="text" id="userId" placeholder="Enter your unique ID" />
267
+ </div>
268
+
269
+ <div class="form-group">
270
+ <label>Cat Gender</label>
271
+ <div class="gender-selector">
272
+ <label class="gender-option selected">
273
+ <input type="radio" name="gender" value="male" checked />
274
+ <div>♂️ Male</div>
275
+ </label>
276
+ <label class="gender-option">
277
+ <input type="radio" name="gender" value="female" />
278
+ <div>♀️ Female</div>
279
+ </label>
280
+ </div>
281
+ </div>
282
+
283
+ <div class="form-group">
284
+ <label for="personality">Cat Personality</label>
285
+ <select id="personality">
286
+ <option value="playful">🎾 Playful (Luna) - Energetic and fun-loving</option>
287
+ <option value="sleepy">😴 Sleepy (Whiskers) - Lazy and cozy</option>
288
+ <option value="sassy">πŸ’… Sassy (Cleo) - Confident with attitude</option>
289
+ <option value="curious">πŸ” Curious (Mittens) - Inquisitive and smart</option>
290
+ <option value="grumpy">😾 Grumpy (Shadow) - Gruff but caring</option>
291
+ </select>
292
+ </div>
293
+
294
+ <div class="personality-info" id="personalityInfo"></div>
295
+
296
+ <div class="form-group">
297
+ <label for="prompt">Message to your cat</label>
298
+ <textarea id="prompt" rows="4" placeholder="Talk to your cat friend..."></textarea>
299
+ </div>
300
+
301
+ <div class="form-group">
302
+ <label for="imageInput">πŸ“· Share an image (optional)</label>
303
+ <input type="file" id="imageInput" accept="image/*"/>
304
+ </div>
305
+
306
+ <button type="submit">πŸ’¬ Send Message</button>
307
+ </form>
308
+
309
+ <div class="output" id="output">Your cat's response will appear here...</div>
310
+ </div>
311
 
312
  <script>
313
+ const personalities = {
314
+ playful: "🎾 Luna is playful and energetic, always ready for fun and adventures!",
315
+ sleepy: "😴 Whiskers is a lazy cat who loves naps and cozy spots.",
316
+ sassy: "πŸ’… Cleo is confident and sassy, with style and attitude!",
317
+ curious: "πŸ” Mittens is inquisitive and loves learning new things.",
318
+ grumpy: "😾 Shadow is grumpy on the outside but secretly cares deeply."
319
+ };
320
+
321
+ const form = document.getElementById('genai-form');
322
+ const out = document.getElementById('output');
323
+ const personalitySelect = document.getElementById('personality');
324
+ const personalityInfo = document.getElementById('personalityInfo');
325
+ const genderOptions = document.querySelectorAll('.gender-option');
326
+
327
+ // Update personality info
328
+ personalitySelect.addEventListener('change', () => {
329
+ personalityInfo.textContent = personalities[personalitySelect.value];
330
+ });
331
+ personalityInfo.textContent = personalities[personalitySelect.value];
332
+
333
+ // Gender selection
334
+ genderOptions.forEach(option => {
335
+ option.addEventListener('click', () => {
336
+ genderOptions.forEach(o => o.classList.remove('selected'));
337
+ option.classList.add('selected');
338
+ option.querySelector('input[type="radio"]').checked = true;
339
+ });
340
+ });
341
+
342
+ form.addEventListener('submit', async e => {
343
+ e.preventDefault();
344
+ const prompt = document.getElementById('prompt').value.trim();
345
+ const uid = document.getElementById('userId').value.trim();
346
+ const personality = personalitySelect.value;
347
+ const gender = document.querySelector('input[name="gender"]:checked').value;
348
+ const fileInput = document.getElementById('imageInput');
349
+
350
+ if (!uid) {
351
+ out.textContent = '❌ Please enter a user ID/token.';
352
+ return;
353
+ }
354
+ if (!prompt && fileInput.files.length === 0) {
355
+ out.textContent = '❌ Enter a message or attach an image.';
356
+ return;
357
+ }
358
+
359
+ out.textContent = '🐱 Your cat is thinking...';
360
+ const formData = new FormData();
361
+ formData.append("text", prompt);
362
+ formData.append("user_id", uid);
363
+ formData.append("personality", personality);
364
+ formData.append("gender", gender);
365
+ if (fileInput.files.length > 0) formData.append("image", fileInput.files[0]);
366
+
367
+ try {
368
+ const resp = await fetch('/generate', { method: 'POST', body: formData });
369
+ const data = await resp.json();
370
+
371
+ if (data.error) {
372
+ out.textContent = '❌ Error: ' + data.error;
373
+ } else {
374
+ // Parse the cat's response
375
+ try {
376
+ const catResponse = JSON.parse(data.result);
377
+ out.textContent =
378
+ `🐱 ${catResponse.text}\\n\\n` +
379
+ `🎬 Animation: ${catResponse.animationId}\\n` +
380
+ `πŸ”Š Sound: ${catResponse.soundType}\\n` +
381
+ `😺 Emotions: ${catResponse.emotion.join(', ')}\\n` +
382
+ `⚧️ Gender: ${gender}\\n\\n` +
383
+ `⏱️ Response time: ${data.timing.total_ms}ms`;
384
+ } catch {
385
+ out.textContent = data.result;
386
+ }
387
+ }
388
+ } catch (err) {
389
+ out.textContent = '❌ Connection error: ' + err.message;
390
+ }
391
+ });
392
  </script>
393
+ </body>
394
+ </html>
395
  """
396
 
397
+ # --- Storyline Fetching ---
398
+ def fetch_current_storyline():
399
+ """Fetch the current day's storyline from the storyline server"""
400
+ try:
401
+ log(f"πŸ“– Fetching current storyline from {STORYLINE_SERVER_URL}")
402
+ resp = requests.get(f"{STORYLINE_SERVER_URL}/current_storyline", timeout=5)
403
+ resp.raise_for_status()
404
+ data = resp.json()
405
+ storyline = data.get("storyline", "No special events today.")
406
+ log(f"βœ… Retrieved storyline: {storyline[:100]}...")
407
+ return storyline
408
+ except Exception as e:
409
+ log(f"⚠️ Failed to fetch storyline: {e}")
410
+ return "It's a normal day in the cat world."
411
+
412
+ def should_inject_storyline(uid, user_data):
413
+ """Check if we should inject the storyline (new day)"""
414
+ current_date = datetime.now(timezone.utc).strftime("%Y-%m-%d")
415
+ last_storyline_date = user_data.get("last_storyline_date", "")
416
+
417
+ if current_date != last_storyline_date:
418
+ log(f"πŸ“… New day detected for {uid}, will inject storyline")
419
+ return True
420
+ return False
421
+
422
+ # --- Gemini Generation with History and System Prompt ---
423
+ def generate_from_gemini(prompt, image_bytes=None, history=None, personality="playful", storyline="", gender="male"):
424
  start_time = time.time()
425
 
426
+ # Get personality details
427
+ personality_info = CAT_PERSONALITIES.get(personality, CAT_PERSONALITIES["playful"])
428
+ personality_traits = f"""
429
+ Name: {personality_info['name']}
430
+ Gender: {gender}
431
+ Description: {personality_info['description']}
432
+ Traits: {personality_info['traits']}
433
+ Speech Style: {personality_info['speech_style']}
434
+ Default Emotions: {', '.join(personality_info['default_emotions'])}
435
+ Default Animation: {personality_info['default_animation']}
436
+ """
437
+
438
+ # Build contents list with system prompt and history
439
  contents = []
440
 
441
+ # Add system prompt as first user message
442
+ system_message = SYSTEM_PROMPT.format(
443
+ personality_traits=personality_traits,
444
+ current_storyline=storyline if storyline else "No special events today."
445
+ )
446
+ contents.append(types.Content(role="user", parts=[types.Part.from_text(text=system_message)]))
447
+
448
+ # Add acknowledgment from model
449
+ contents.append(types.Content(role="model", parts=[types.Part.from_text(
450
+ text='{"text": "Understood! I will respond as a cat with the specified personality in JSON format.", "soundType": "happyMeow", "emotion": ["happy"], "animationId": "talking"}'
451
+ )]))
452
+
453
  # Add historical messages (limit to recent turns to avoid token limits)
454
  if history:
455
  recent_history = history[-MAX_HISTORY_TURNS:]
456
+ log(f"πŸ“š Using {len(recent_history)} history entries for context")
457
  for entry in recent_history:
458
  # Add user message
459
  user_parts = [types.Part.from_text(text=entry["prompt"])]
 
463
  model_parts = [types.Part.from_text(text=entry["response"])]
464
  contents.append(types.Content(role="model", parts=model_parts))
465
  else:
466
+ log("πŸ“š No history available for context")
467
 
468
  # Add current user message
469
  current_parts = []
 
474
 
475
  contents.append(types.Content(role="user", parts=current_parts))
476
 
477
+ # Force JSON output with schema
478
+ cfg = types.GenerateContentConfig(
479
+ response_mime_type="application/json",
480
+ response_schema={
481
+ "type": "object",
482
+ "properties": {
483
+ "text": {"type": "string"},
484
+ "soundType": {"type": "string"},
485
+ "emotion": {"type": "array", "items": {"type": "string"}},
486
+ "animationId": {"type": "string"}
487
+ },
488
+ "required": ["text", "soundType", "emotion", "animationId"]
489
+ }
490
+ )
491
 
492
  model_start = time.time()
493
  res = client.models.generate_content(
 
515
  if now - last_activity >= MEMORY_CLEANUP_TIMEOUT:
516
  del user_memory[uid]
517
  removed_count += 1
518
+ log(f"🧹 Cleaned up inactive user {uid}")
 
519
 
520
  if removed_count > 0:
521
+ log(f"🧹 Cleaned up {removed_count} inactive user(s)")
522
  return removed_count
523
 
524
  # --- History Management ---
525
  def get_user_history(uid):
526
  """Fetch user history from memory or backend"""
527
  if uid not in user_memory:
528
+ log(f"πŸ” User {uid} not in memory, fetching from backend...")
529
  try:
530
  fetch_url = f"{LAMBDA_URL}?userid={uid}"
531
+ log(f"πŸ“‘ Fetching from: {fetch_url}")
532
  resp = requests.get(fetch_url, timeout=5)
533
+ log(f"πŸ“‘ Response status: {resp.status_code}")
534
  resp.raise_for_status()
535
 
536
  response_data = resp.json()
537
+ log(f"πŸ“‘ Response data keys: {list(response_data.keys())}")
538
 
539
  loaded_history = response_data.get("history", [])
540
+ loaded_personality = response_data.get("personality", "playful")
541
+ loaded_gender = response_data.get("gender", "male")
542
+ loaded_last_storyline = response_data.get("last_storyline_date", "")
543
+
544
+ log(f"βœ… Loaded {len(loaded_history)} messages from backend for {uid}")
545
 
546
  # Only keep the most recent MAX_MEMORY_MESSAGES when loading
547
  user_memory[uid] = {
548
  "history": loaded_history[-MAX_MEMORY_MESSAGES:],
549
  "last_sync": time.time(),
550
  "last_activity": time.time(),
551
+ "needs_sync": False,
552
+ "personality": loaded_personality,
553
+ "gender": loaded_gender,
554
+ "last_storyline_date": loaded_last_storyline
555
  }
 
556
  except Exception as e:
557
+ log(f"❌ Failed to load history for {uid}: {e}")
 
558
  user_memory[uid] = {
559
  "history": [],
560
  "last_sync": time.time(),
561
  "last_activity": time.time(),
562
+ "needs_sync": False,
563
+ "personality": "playful",
564
+ "gender": "male",
565
+ "last_storyline_date": ""
566
  }
567
  else:
568
+ log(f"βœ… User {uid} already in memory with {len(user_memory[uid]['history'])} messages")
569
 
570
  # Update last activity timestamp
571
  user_memory[uid]["last_activity"] = time.time()
572
+ return user_memory[uid]
573
 
574
+ def update_user_history(uid, prompt, response, personality="playful", gender="male"):
575
  """Add new message to user history"""
576
  entry = {"prompt": prompt, "response": response, "timestamp": time.time()}
577
+ current_date = datetime.now(timezone.utc).strftime("%Y-%m-%d")
578
+
579
  if uid not in user_memory:
580
  user_memory[uid] = {
581
  "history": [],
582
  "last_sync": time.time(),
583
  "last_activity": time.time(),
584
+ "needs_sync": False,
585
+ "personality": personality,
586
+ "gender": gender,
587
+ "last_storyline_date": current_date
588
  }
589
 
590
  user_memory[uid]["history"].append(entry)
591
  user_memory[uid]["last_activity"] = time.time()
592
+ user_memory[uid]["needs_sync"] = True
593
+ user_memory[uid]["personality"] = personality
594
+ user_memory[uid]["gender"] = gender
595
+ user_memory[uid]["last_storyline_date"] = current_date
596
 
597
+ log(f"πŸ’Ύ Updated history for {uid}, now has {len(user_memory[uid]['history'])} messages")
598
 
599
  # Trim history to MAX_MEMORY_MESSAGES to prevent unbounded growth
 
600
  if len(user_memory[uid]["history"]) > MAX_MEMORY_MESSAGES:
601
  user_memory[uid]["history"] = user_memory[uid]["history"][-MAX_MEMORY_MESSAGES:]
602
+ log(f"βœ‚οΈ Trimmed history for {uid} to {MAX_MEMORY_MESSAGES} messages")
 
603
 
604
  # --- Routes ---
605
  @app.route("/")
606
  def index():
607
  return render_template_string(HTML)
608
 
 
609
  @app.route("/cron/sync", methods=["GET", "POST"])
610
  def remote_saving():
611
  """Cron job endpoint for syncing user memory to backend"""
612
+ log("πŸ”„ Cron sync started")
613
  now = time.time()
614
  synced_users = []
615
  failed_users = []
 
620
 
621
  # Then sync only users that need syncing (have new messages)
622
  for uid, data in list(user_memory.items()):
 
623
  needs_sync = data.get("needs_sync", False)
624
  time_since_last_sync = now - data.get("last_sync", 0)
625
 
626
  if not needs_sync:
627
  skipped_users.append(uid)
628
+ log(f"⏭️ Skipping {uid} - no new messages")
629
  continue
630
 
631
  if time_since_last_sync < FLUSH_INTERVAL:
632
  skipped_users.append(uid)
633
+ log(f"⏭️ Skipping {uid} - synced {int(time_since_last_sync)}s ago")
634
  continue
635
 
636
  if data["history"]:
637
  try:
 
638
  history_to_sync = data["history"][-MAX_MEMORY_MESSAGES:]
639
+ payload = {
640
+ "user_id": uid,
641
+ "history": history_to_sync,
642
+ "personality": data.get("personality", "playful"),
643
+ "gender": data.get("gender", "male"),
644
+ "last_storyline_date": data.get("last_storyline_date", "")
645
+ }
646
+ log(f"πŸ”„ Syncing {uid} ({len(history_to_sync)} messages)")
647
  resp = requests.post(LAMBDA_URL, json=payload, timeout=5)
648
  resp.raise_for_status()
649
  user_memory[uid]["last_sync"] = now
650
+ user_memory[uid]["needs_sync"] = False
651
+ log(f"βœ… Successfully synced {uid}")
 
652
  synced_users.append(uid)
653
  except Exception as e:
654
+ log(f"❌ Failed sync for {uid}: {e}")
 
655
  failed_users.append({"user_id": uid, "error": str(e)})
656
 
657
+ result = {
658
  "success": True,
659
  "synced_count": len(synced_users),
660
  "failed_count": len(failed_users),
 
663
  "failed_users": failed_users,
664
  "skipped_users": skipped_users,
665
  "active_users_in_memory": len(user_memory)
666
+ }
667
+ log(f"βœ… Cron sync completed: {result}")
668
+ return jsonify(result), 200
669
 
670
  @app.route("/generate", methods=["POST"])
671
  def gen():
672
  uid = request.form.get("user_id", "").strip()
673
+ personality = request.form.get("personality", "playful").strip()
674
+ gender = request.form.get("gender", "male").strip()
675
+
676
  if not uid:
677
  return jsonify({"error": "Missing user ID/token"}), 400
678
+
679
+ if personality not in CAT_PERSONALITIES:
680
+ personality = "playful"
681
+
682
+ if gender not in ["male", "female"]:
683
+ gender = "male"
684
 
685
  prompt = request.form.get("text", "")
686
  image = request.files.get("image")
687
  img_bytes = image.read() if image else None
688
+
689
  if not prompt and not img_bytes:
690
  return jsonify({"error": "No prompt or image provided"}), 400
691
 
692
  try:
693
+ log(f"{'='*50}")
694
+ log(f"πŸ†• New request from {uid} with {personality} personality ({gender})")
695
 
696
+ # Load user's data
697
+ user_data = get_user_history(uid)
698
+ history = user_data["history"]
699
+ log(f"πŸ“– Retrieved {len(history)} messages from history")
700
 
701
+ # Check if we need to inject storyline (new day)
702
+ storyline = ""
703
+ if should_inject_storyline(uid, user_data):
704
+ storyline = fetch_current_storyline()
705
+ log(f"πŸ“– Injecting storyline for new day")
706
 
707
+ # Generate response
708
+ result = generate_from_gemini(prompt, img_bytes, history=history, personality=personality, storyline=storyline, gender=gender)
709
+
710
+ # Update history
711
+ update_user_history(uid, prompt, result["text"], personality, gender)
712
+ log(f"{'='*50}")
713
 
714
  return jsonify({"result": result["text"], "timing": result["timing"]})
715
  except Exception as e:
716
+ log(f"❌ Generation failed: {e}")
717
+ logger.exception("Full traceback:")
718
  return jsonify({"error": str(e)}), 500
719
 
 
720
  if __name__ == "__main__":
721
+ log("πŸš€ Starting Cat Companion Server...")
722
+ log(f"πŸ“ Lambda URL: {LAMBDA_URL}")
723
+ log(f"πŸ“– Storyline Server: {STORYLINE_SERVER_URL}")
724
+ log(f"βš™οΈ Max history turns: {MAX_HISTORY_TURNS}")
725
+ log(f"βš™οΈ Max memory messages: {MAX_MEMORY_MESSAGES}")
726
+ log(f"🐱 Available personalities: {', '.join(CAT_PERSONALITIES.keys())}")
727
+ log(f"🎬 Available animations: {', '.join([anim for anims in ANIMATION_IDS.values() for anim in anims])}")
728
  port = int(os.getenv("PORT", 7860))
729
  app.run(host="0.0.0.0", port=port)