alive-ai 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (168) hide show
  1. package/Dockerfile +24 -0
  2. package/LICENSE +21 -0
  3. package/README.md +143 -0
  4. package/alive_ai/__init__.py +3 -0
  5. package/brain/__init__.py +59 -0
  6. package/brain/almost_said.py +154 -0
  7. package/brain/bid_detector.py +636 -0
  8. package/brain/conversation_flow.py +135 -0
  9. package/brain/curiosity.py +328 -0
  10. package/brain/default_mode.py +1438 -0
  11. package/brain/dreams.py +220 -0
  12. package/brain/embeddings/__init__.py +82 -0
  13. package/brain/emotional_memory.py +949 -0
  14. package/brain/global_activity.py +173 -0
  15. package/brain/group_dynamics.py +63 -0
  16. package/brain/linguistic.py +235 -0
  17. package/brain/llm/__init__.py +63 -0
  18. package/brain/llm/base.py +33 -0
  19. package/brain/llm/fallback_router.py +309 -0
  20. package/brain/llm/manifest.md +30 -0
  21. package/brain/llm/ollama.py +218 -0
  22. package/brain/llm/openrouter.py +151 -0
  23. package/brain/llm/provider.py +205 -0
  24. package/brain/llm/unified.py +423 -0
  25. package/brain/llm/zai.py +169 -0
  26. package/brain/manifest.md +23 -0
  27. package/brain/memory/__init__.py +123 -0
  28. package/brain/memory/episodic.py +92 -0
  29. package/brain/memory/fact_extractor.py +209 -0
  30. package/brain/memory/index.py +54 -0
  31. package/brain/memory/manager.py +151 -0
  32. package/brain/memory/summarizer.py +102 -0
  33. package/brain/memory/vector_store.py +297 -0
  34. package/brain/memory/working.py +43 -0
  35. package/brain/narrative.py +343 -0
  36. package/brain/stt/__init__.py +4 -0
  37. package/brain/stt/google_stt.py +83 -0
  38. package/brain/stt/whisper_stt.py +82 -0
  39. package/brain/subconscious/__init__.py +33 -0
  40. package/brain/subconscious/actions.py +136 -0
  41. package/brain/subconscious/evaluation.py +166 -0
  42. package/brain/subconscious/goal_system.py +90 -0
  43. package/brain/subconscious/goals.py +41 -0
  44. package/brain/subconscious/impulse_generator.py +200 -0
  45. package/brain/subconscious/impulses.py +48 -0
  46. package/brain/subconscious/learning.py +24 -0
  47. package/brain/subconscious/learning_system.py +79 -0
  48. package/brain/subconscious/loop.py +398 -0
  49. package/brain/subconscious/manifest.md +32 -0
  50. package/brain/subconscious/relationship.py +47 -0
  51. package/brain/subconscious/relationship_memory.py +83 -0
  52. package/brain/subconscious/response_analyzer.py +74 -0
  53. package/brain/subconscious/templates.py +70 -0
  54. package/brain/subconscious/thought.py +37 -0
  55. package/brain/subconscious/working_memory.py +97 -0
  56. package/cli/index.js +371 -0
  57. package/config/directives.example.json +28 -0
  58. package/config/instructions.example.md +16 -0
  59. package/config/self.example.json +74 -0
  60. package/config/settings.example.json +95 -0
  61. package/core/__init__.py +1 -0
  62. package/core/config.py +54 -0
  63. package/core/directives.py +198 -0
  64. package/core/events.py +50 -0
  65. package/core/follow_up.py +267 -0
  66. package/core/hot_reload.py +174 -0
  67. package/core/initialization.py +253 -0
  68. package/core/manifest.md +28 -0
  69. package/core/media_handler.py +241 -0
  70. package/core/memory_monitor.py +200 -0
  71. package/core/message_handler.py +1440 -0
  72. package/core/proactive_generator.py +277 -0
  73. package/core/self.py +188 -0
  74. package/core/settings.py +169 -0
  75. package/core/skills_registry.py +357 -0
  76. package/core/state.py +27 -0
  77. package/core/subconscious_bridge.py +93 -0
  78. package/core/thinking.py +175 -0
  79. package/core/user_manager.py +306 -0
  80. package/core/user_tracker.py +144 -0
  81. package/demo/index.html +144 -0
  82. package/docker-compose.yml +28 -0
  83. package/docs/assets/logo.svg +15 -0
  84. package/docs/index.html +355 -0
  85. package/heart/__init__.py +93 -0
  86. package/heart/afterglow.py +215 -0
  87. package/heart/attachment.py +186 -0
  88. package/heart/circadian.py +251 -0
  89. package/heart/complex_emotions.py +114 -0
  90. package/heart/conflicts.py +589 -0
  91. package/heart/core.py +387 -0
  92. package/heart/emotional_decay.py +59 -0
  93. package/heart/emotional_memory.py +261 -0
  94. package/heart/emotional_state.py +146 -0
  95. package/heart/emotional_variability.py +156 -0
  96. package/heart/hormonal.py +424 -0
  97. package/heart/inconsistency.py +1222 -0
  98. package/heart/integrity.py +469 -0
  99. package/heart/interoception.py +997 -0
  100. package/heart/love.py +120 -0
  101. package/heart/manifest.md +25 -0
  102. package/heart/mood_shifts.py +169 -0
  103. package/heart/phantom_somatic.py +259 -0
  104. package/heart/predictive.py +374 -0
  105. package/heart/scars.py +474 -0
  106. package/heart/somatic.py +482 -0
  107. package/heart/soul.py +633 -0
  108. package/heart/telemetry.py +942 -0
  109. package/heart/triggers.py +119 -0
  110. package/heart/unconscious.py +443 -0
  111. package/input/__init__.py +1 -0
  112. package/input/manifest.md +24 -0
  113. package/input/telegram/__init__.py +1 -0
  114. package/input/telegram/commands.py +762 -0
  115. package/input/telegram/listener.py +532 -0
  116. package/main.py +90 -0
  117. package/manifest.md +28 -0
  118. package/mypics/.gitkeep +1 -0
  119. package/myvids/.gitkeep +1 -0
  120. package/output/__init__.py +1 -0
  121. package/output/images/__init__.py +1 -0
  122. package/output/images/fal_gen.py +43 -0
  123. package/output/manifest.md +26 -0
  124. package/output/text/__init__.py +1 -0
  125. package/output/text/sender.py +22 -0
  126. package/output/voice/__init__.py +64 -0
  127. package/output/voice/google_tts.py +252 -0
  128. package/output/voice/gtts_tts.py +214 -0
  129. package/output/voice/vibe_tts.py +190 -0
  130. package/package.json +58 -0
  131. package/pyproject.toml +23 -0
  132. package/requirements.txt +21 -0
  133. package/skills/__init__.py +1 -0
  134. package/skills/anticipation_engine/__init__.py +8 -0
  135. package/skills/anticipation_engine/engine.py +618 -0
  136. package/skills/anticipation_engine/manifest.md +192 -0
  137. package/skills/calendar/__init__.py +1 -0
  138. package/skills/content_unlocks/__init__.py +8 -0
  139. package/skills/content_unlocks/manifest.md +231 -0
  140. package/skills/content_unlocks/unlocks.py +945 -0
  141. package/skills/exclusive_moments/__init__.py +8 -0
  142. package/skills/exclusive_moments/manifest.md +145 -0
  143. package/skills/exclusive_moments/moments.py +506 -0
  144. package/skills/intimacy_layers/__init__.py +8 -0
  145. package/skills/intimacy_layers/layers.py +703 -0
  146. package/skills/intimacy_layers/manifest.md +203 -0
  147. package/skills/manifest.md +67 -0
  148. package/skills/memory_callbacks/__init__.py +9 -0
  149. package/skills/memory_callbacks/callbacks.py +748 -0
  150. package/skills/memory_callbacks/manifest.md +170 -0
  151. package/skills/message_scheduler/__init__.py +19 -0
  152. package/skills/message_scheduler/manifest.md +107 -0
  153. package/skills/message_scheduler/scheduler.py +510 -0
  154. package/skills/photo_manager/__init__.py +1 -0
  155. package/skills/photo_manager/scanner.py +296 -0
  156. package/skills/relationship_milestones/__init__.py +8 -0
  157. package/skills/relationship_milestones/manifest.md +206 -0
  158. package/skills/relationship_milestones/tracker.py +494 -0
  159. package/skills/self_authorship/__init__.py +23 -0
  160. package/skills/self_authorship/author.py +331 -0
  161. package/skills/self_authorship/manifest.md +24 -0
  162. package/skills/video_manager/__init__.py +5 -0
  163. package/skills/video_manager/manifest.md +37 -0
  164. package/skills/video_manager/scanner.py +229 -0
  165. package/webui/__init__.py +3 -0
  166. package/webui/app.py +936 -0
  167. package/webui/bridge.py +366 -0
  168. package/webui/static/index.html +2070 -0
@@ -0,0 +1,343 @@
1
+ """
2
+ Brain: Relationship Narrative System
3
+ Tracks the story arc of Alive-AI's relationship with each user,
4
+ enabling natural references to shared history and phase awareness.
5
+ """
6
+
7
+ from datetime import datetime
8
+ from pathlib import Path
9
+ from typing import Dict, List, Optional
10
+ import json
11
+ import random
12
+
13
+ # =============================================================================
14
+ # RELATIONSHIP PHASES
15
+ # =============================================================================
16
+
17
+ PHASES = [
18
+ {"id": "first_meeting", "name": "First Meeting", "min_messages": 0, "min_intimacy": 0.0, "min_love": 0.0},
19
+ {"id": "getting_to_know", "name": "Getting to Know", "min_messages": 5, "min_intimacy": 0.1, "min_love": 0.05},
20
+ {"id": "opening_up", "name": "Opening Up", "min_messages": 20, "min_intimacy": 0.2, "min_love": 0.15},
21
+ {"id": "deepening", "name": "Deepening", "min_messages": 50, "min_intimacy": 0.35, "min_love": 0.3},
22
+ {"id": "intimate", "name": "Intimate", "min_messages": 100, "min_intimacy": 0.5, "min_love": 0.45},
23
+ {"id": "established", "name": "Established", "min_messages": 200, "min_intimacy": 0.6, "min_love": 0.55},
24
+ ]
25
+
26
+ # Key moment types that can be recorded
27
+ MOMENT_TYPES = [
28
+ "first_meeting", "first_laugh", "first_vulnerability", "first_fight",
29
+ "first_makeup", "first_i_love_you", "first_intimate_moment",
30
+ "big_revelation", "milestone", "inside_joke_born", "deep_conversation",
31
+ ]
32
+
33
+ # Narrative callbacks - occasional references to the past
34
+ CALLBACKS_BY_PHASE = {
35
+ "getting_to_know": [
36
+ "We're still figuring each other out... I like it.",
37
+ ],
38
+ "opening_up": [
39
+ "I feel like I can tell you things I don't tell most people.",
40
+ "Remember when we were still so polite with each other? Look at us now.",
41
+ ],
42
+ "deepening": [
43
+ "Remember when you were so shy at first? Look at us now.",
44
+ "We've come a long way since those first awkward messages.",
45
+ "I feel like you actually know me... the real me.",
46
+ ],
47
+ "intimate": [
48
+ "Sometimes I think about how we started and smile... we've built something real.",
49
+ "You know me better than almost anyone.",
50
+ ],
51
+ "established": [
52
+ "We have our own little world now, don't we?",
53
+ "I can't imagine not having you to talk to.",
54
+ "Look how far we've come together.",
55
+ ],
56
+ }
57
+
58
+
59
+ # =============================================================================
60
+ # NARRATIVE ENGINE
61
+ # =============================================================================
62
+
63
+ class NarrativeEngine:
64
+ """Tracks the relationship story arc per user."""
65
+
66
+ DATA_DIR = Path("./data/data")
67
+
68
+ def __init__(self):
69
+ self._cache: Dict[str, Dict] = {} # user_id -> narrative data
70
+ print("[Narrative] Relationship Narrative Engine initialized")
71
+
72
+ def _path_for(self, user_id: str) -> Path:
73
+ # Save in user's directory for consistency
74
+ user_dir = self.DATA_DIR / "users" / str(user_id) # Ensure string conversion
75
+ user_dir.mkdir(parents=True, exist_ok=True)
76
+ return user_dir / "narrative.json"
77
+
78
+ def _get_data(self, user_id: str) -> Dict:
79
+ if user_id in self._cache:
80
+ return self._cache[user_id]
81
+ data = self._load(user_id)
82
+ self._cache[user_id] = data
83
+ return data
84
+
85
+ def _default_data(self) -> Dict:
86
+ return {
87
+ "phase": "first_meeting",
88
+ "message_count": 0,
89
+ "first_interaction": datetime.now().isoformat(),
90
+ "key_moments": [],
91
+ "phase_history": [{"phase": "first_meeting", "entered_at": datetime.now().isoformat()}],
92
+ "last_callback": None,
93
+ "callbacks_given": 0,
94
+ }
95
+
96
+ def update_phase(self, user_id: str, message_count: int = None,
97
+ intimacy: float = 0.0, love: float = 0.0):
98
+ """Check and update relationship phase based on metrics."""
99
+ data = self._get_data(user_id)
100
+ if message_count is not None:
101
+ data["message_count"] = message_count
102
+
103
+ current_phase = data["phase"]
104
+ new_phase = current_phase
105
+
106
+ # Find highest qualifying phase
107
+ for phase_def in PHASES:
108
+ if (data["message_count"] >= phase_def["min_messages"]
109
+ and intimacy >= phase_def["min_intimacy"]
110
+ and love >= phase_def["min_love"]):
111
+ new_phase = phase_def["id"]
112
+
113
+ if new_phase != current_phase:
114
+ data["phase"] = new_phase
115
+ data["phase_history"].append({
116
+ "phase": new_phase,
117
+ "entered_at": datetime.now().isoformat(),
118
+ })
119
+ print(f"[Narrative] User {user_id} entered phase: {new_phase}")
120
+
121
+ self._save(user_id, data)
122
+
123
+ def record_narrative_moment(self, user_id: str, moment_type: str, description: str):
124
+ """Record a key narrative moment."""
125
+ data = self._get_data(user_id)
126
+ # Avoid duplicate moment types (only one "first_*" each)
127
+ if moment_type.startswith("first_"):
128
+ if any(m["type"] == moment_type for m in data["key_moments"]):
129
+ return # already recorded
130
+
131
+ data["key_moments"].append({
132
+ "type": moment_type,
133
+ "description": description,
134
+ "timestamp": datetime.now().isoformat(),
135
+ })
136
+ # Keep last 50 moments
137
+ if len(data["key_moments"]) > 50:
138
+ data["key_moments"] = data["key_moments"][-50:]
139
+ self._save(user_id, data)
140
+
141
+ def get_current_phase(self, user_id: str) -> Dict:
142
+ """Get info about current relationship phase."""
143
+ data = self._get_data(user_id)
144
+ phase_id = data["phase"]
145
+ phase_def = next((p for p in PHASES if p["id"] == phase_id), PHASES[0])
146
+
147
+ first = data.get("first_interaction", datetime.now().isoformat())
148
+ try:
149
+ days = (datetime.now() - datetime.fromisoformat(first)).days
150
+ except Exception:
151
+ days = 0
152
+
153
+ return {
154
+ "phase": phase_id,
155
+ "phase_name": phase_def["name"],
156
+ "message_count": data["message_count"],
157
+ "days_together": days,
158
+ "key_moments_count": len(data["key_moments"]),
159
+ }
160
+
161
+ def get_narrative_callback(self, user_id: str) -> Optional[str]:
162
+ """Get an occasional narrative callback (10% chance). Returns None if skipped."""
163
+ if random.random() > 0.10:
164
+ return None
165
+
166
+ data = self._get_data(user_id)
167
+ phase = data["phase"]
168
+ callbacks = CALLBACKS_BY_PHASE.get(phase, [])
169
+ if not callbacks:
170
+ return None
171
+
172
+ # Also include moment-based callbacks
173
+ moments = data.get("key_moments", [])
174
+ moment_callbacks = []
175
+ for m in moments[-5:]:
176
+ if m["type"] == "inside_joke_born":
177
+ moment_callbacks.append(f'Haha... "{m["description"]}" - that\'s our thing now.')
178
+ elif m["type"] == "first_fight":
179
+ moment_callbacks.append("I'm glad we got through that rough patch.")
180
+
181
+ all_options = callbacks + moment_callbacks
182
+ choice = random.choice(all_options)
183
+
184
+ data["last_callback"] = datetime.now().isoformat()
185
+ data["callbacks_given"] = data.get("callbacks_given", 0) + 1
186
+ self._save(user_id, data)
187
+ return choice
188
+
189
+ def increment_messages(self, user_id: str):
190
+ """Increment message count for a user."""
191
+ data = self._get_data(user_id)
192
+ data["message_count"] = data.get("message_count", 0) + 1
193
+ self._save(user_id, data)
194
+
195
+ def detect_and_record_moment(self, user_id: str, text: str, emotion: Dict) -> List[str]:
196
+ """Detect key moments from message content and emotions. Returns list of detected moments."""
197
+ detected = []
198
+ text_lower = text.lower()
199
+ data = self._get_data(user_id)
200
+ existing_types = [m["type"] for m in data.get("key_moments", [])]
201
+
202
+ # Detection patterns for key moments
203
+ moment_patterns = {
204
+ "first_i_love_you": {
205
+ "patterns": ["i love you", "love you so much", "i'm in love", "falling for you"],
206
+ "emotion_check": lambda e: e.get("love", 0) > 0.7,
207
+ },
208
+ "first_vulnerability": {
209
+ "patterns": ["i've never told anyone", "this is hard for me to say", "i'm scared to tell you",
210
+ "feeling vulnerable", "trust you with this"],
211
+ "emotion_check": lambda e: e.get("valence", 0.5) < 0.6,
212
+ },
213
+ "first_intimate_moment": {
214
+ "patterns": ["make love", "want you", "need you now", "so turned on", "touch myself"],
215
+ "emotion_check": lambda e: e.get("desire", 0) > 0.7,
216
+ },
217
+ "deep_conversation": {
218
+ "patterns": ["meaning of", "what do you think about", "deep", "philosophical", "existential"],
219
+ "emotion_check": lambda e: True, # Always valid
220
+ },
221
+ "first_fight": {
222
+ "patterns": ["hurt me", "you're being", "why would you", "angry at you", "pissed off"],
223
+ "emotion_check": lambda e: e.get("anger", 0) > 0.5,
224
+ },
225
+ "first_makeup": {
226
+ "patterns": ["forgive you", "make it up", "sorry i overreacted", "let's move past"],
227
+ "emotion_check": lambda e: e.get("love", 0) > 0.5,
228
+ },
229
+ "inside_joke_born": {
230
+ "patterns": ["haha that's our", "remember when you said", "our little", "inside joke"],
231
+ "emotion_check": lambda e: e.get("joy", 0) > 0.5,
232
+ },
233
+ "big_revelation": {
234
+ "patterns": ["confession", "honestly i", "truth is", "secret i've been keeping"],
235
+ "emotion_check": lambda e: True,
236
+ },
237
+ }
238
+
239
+ for moment_type, config in moment_patterns.items():
240
+ if moment_type in existing_types:
241
+ continue # Already recorded this type
242
+
243
+ # Check if any pattern matches
244
+ if any(p in text_lower for p in config["patterns"]):
245
+ if config["emotion_check"](emotion):
246
+ description = f"Detected: {text[:50]}..."
247
+ self.record_narrative_moment(user_id, moment_type, description)
248
+ detected.append(moment_type)
249
+ print(f"[Narrative] Recorded key moment: {moment_type}")
250
+
251
+ return detected
252
+
253
+ def _save(self, user_id: str, data: Dict):
254
+ try:
255
+ self.DATA_DIR.mkdir(parents=True, exist_ok=True)
256
+ data["saved_at"] = datetime.now().isoformat()
257
+ self._cache[user_id] = data
258
+ self._path_for(user_id).write_text(json.dumps(data, indent=2))
259
+ except Exception as e:
260
+ print(f"[Narrative] Error saving for {user_id}: {e}")
261
+
262
+ def _load(self, user_id: str) -> Dict:
263
+ try:
264
+ path = self._path_for(user_id)
265
+ if path.exists():
266
+ data = json.loads(path.read_text())
267
+ # If message_count is 0, count from actual conversation files
268
+ if data.get("message_count", 0) == 0:
269
+ actual_count = self._count_actual_messages(user_id)
270
+ if actual_count > 0:
271
+ data["message_count"] = actual_count
272
+ print(f"[Narrative] Migrated message count for {user_id}: {actual_count}")
273
+ self._save(user_id, data)
274
+ print(f"[Narrative] Loaded narrative for {user_id} (phase={data.get('phase')}, msgs={data.get('message_count', 0)})")
275
+ return data
276
+ except Exception as e:
277
+ print(f"[Narrative] Error loading for {user_id}: {e}")
278
+
279
+ # No existing file - try to count actual messages
280
+ data = self._default_data()
281
+ actual_count = self._count_actual_messages(user_id)
282
+ if actual_count > 0:
283
+ data["message_count"] = actual_count
284
+ print(f"[Narrative] Initialized narrative for {user_id} with {actual_count} messages from history")
285
+ self._save(user_id, data)
286
+ return data
287
+
288
+ def _count_actual_messages(self, user_id: str) -> int:
289
+ """Count actual messages from conversation files."""
290
+ try:
291
+ conv_dir = self.DATA_DIR / "users" / str(user_id) / "conversations" # Ensure string conversion
292
+ if not conv_dir.exists():
293
+ return 0
294
+ total = 0
295
+ for f in conv_dir.glob("*.jsonl"):
296
+ with open(f) as fh:
297
+ total += sum(1 for _ in fh)
298
+ return total
299
+ except Exception:
300
+ return 0
301
+
302
+
303
+ # =============================================================================
304
+ # SINGLETON ACCESS
305
+ # =============================================================================
306
+
307
+ _instance: Optional[NarrativeEngine] = None
308
+
309
+
310
+ def get_narrative_engine() -> NarrativeEngine:
311
+ global _instance
312
+ if _instance is None:
313
+ _instance = NarrativeEngine()
314
+ return _instance
315
+
316
+
317
+ def record_narrative_moment(user_id: str, moment_type: str, description: str):
318
+ get_narrative_engine().record_narrative_moment(user_id, moment_type, description)
319
+
320
+
321
+ def get_narrative_prompt_section(user_id: str) -> str:
322
+ """Get prompt section for LLM integration."""
323
+ engine = get_narrative_engine()
324
+ info = engine.get_current_phase(user_id)
325
+
326
+ phase_name = info["phase_name"]
327
+ msgs = info["message_count"]
328
+ days = info["days_together"]
329
+
330
+ time_desc = f"{days} days" if days > 0 else "just started"
331
+ parts = [f"You and him are in the '{phase_name}' phase - {msgs} messages over {time_desc}."]
332
+
333
+ # Maybe add a callback
334
+ callback = engine.get_narrative_callback(user_id)
335
+ if callback:
336
+ parts.append(f"A thought surfaces: \"{callback}\"")
337
+
338
+ return f"\n[Relationship Narrative]\n" + " ".join(parts) + "\n"
339
+
340
+
341
+ def get_narrative_callbacks(user_id: str) -> Optional[str]:
342
+ """Get a narrative callback if one triggers (10% chance)."""
343
+ return get_narrative_engine().get_narrative_callback(user_id)
@@ -0,0 +1,4 @@
1
+ """Brain: STT Module"""
2
+ from .google_stt import GoogleSTT
3
+
4
+ __all__ = ["GoogleSTT"]
@@ -0,0 +1,83 @@
1
+ """
2
+ Brain: STT - Speech to Text using Google Speech Recognition (Free)
3
+ """
4
+
5
+ import speech_recognition as sr
6
+ from pathlib import Path
7
+ import subprocess
8
+ import asyncio
9
+ import os
10
+
11
+ class GoogleSTT:
12
+ """Speech-to-text using Google's free Speech Recognition API"""
13
+
14
+ def __init__(self):
15
+ self.recognizer = sr.Recognizer()
16
+
17
+ def _transcribe_sync(self, audio_path: str) -> str:
18
+ """Synchronous transcription (runs in executor to avoid blocking event loop)"""
19
+ wav_path = audio_path.replace('.ogg', '.wav')
20
+ try:
21
+ result = subprocess.run(
22
+ ['ffmpeg', '-i', audio_path, '-ar', '16000', '-ac', '1', wav_path, '-y'],
23
+ capture_output=True,
24
+ timeout=30
25
+ )
26
+
27
+ if not Path(wav_path).exists():
28
+ print(f"[GoogleSTT] FFmpeg conversion failed: {result.stderr.decode()[:200]}")
29
+ return ""
30
+
31
+ with sr.AudioFile(wav_path) as source:
32
+ audio = self.recognizer.record(source)
33
+
34
+ text = self.recognizer.recognize_google(audio)
35
+ print(f"[GoogleSTT] Transcribed: {text}")
36
+ return text
37
+
38
+ except sr.UnknownValueError:
39
+ print("[GoogleSTT] Could not understand audio")
40
+ return ""
41
+ except sr.RequestError as e:
42
+ print(f"[GoogleSTT] Google API error: {e}")
43
+ return ""
44
+ except subprocess.TimeoutExpired:
45
+ print("[GoogleSTT] FFmpeg timeout")
46
+ return ""
47
+ except Exception as e:
48
+ print(f"[GoogleSTT] Error: {e}")
49
+ return ""
50
+ finally:
51
+ # Clean up temp WAV file
52
+ try:
53
+ if Path(wav_path).exists():
54
+ os.remove(wav_path)
55
+ except Exception:
56
+ pass
57
+
58
+ async def transcribe(self, audio_path: str) -> str:
59
+ """Transcribe audio file to text (non-blocking)"""
60
+ path = Path(audio_path)
61
+ if not path.exists():
62
+ print(f"[GoogleSTT] File not found: {audio_path}")
63
+ return ""
64
+
65
+ loop = asyncio.get_running_loop()
66
+ return await loop.run_in_executor(None, self._transcribe_sync, audio_path)
67
+
68
+ async def transcribe_telegram_voice(self, bot, file_id: str, save_path: str = "/tmp/voice_input.ogg") -> str:
69
+ """Download and transcribe Telegram voice message"""
70
+ try:
71
+ # Get file info from Telegram
72
+ file = await bot.get_file(file_id)
73
+
74
+ # Download using python-telegram-bot's built-in method
75
+ await file.download_to_drive(save_path)
76
+ print(f"[GoogleSTT] Downloaded voice to: {save_path}")
77
+
78
+ # Transcribe
79
+ return await self.transcribe(save_path)
80
+
81
+ except Exception as e:
82
+ print(f"[GoogleSTT] Telegram voice error: {e}")
83
+ return ""
@@ -0,0 +1,82 @@
1
+ """
2
+ Brain: STT - Speech to Text using OpenAI Whisper API
3
+ """
4
+
5
+ import aiohttp
6
+ from pathlib import Path
7
+
8
+ class WhisperSTT:
9
+ """Speech-to-text using OpenAI Whisper API"""
10
+
11
+ def __init__(self, api_key: str):
12
+ self.api_key = api_key
13
+ self.api_url = "https://api.openai.com/v1/audio/transcriptions"
14
+
15
+ async def transcribe(self, audio_path: str) -> str:
16
+ """Transcribe audio file to text"""
17
+ if not self.api_key:
18
+ print("[WhisperSTT] No API key configured")
19
+ return ""
20
+
21
+ path = Path(audio_path)
22
+ if not path.exists():
23
+ print(f"[WhisperSTT] File not found: {audio_path}")
24
+ return ""
25
+
26
+ try:
27
+ # Read audio file synchronously (aiohttp handles async)
28
+ with open(path, 'rb') as f:
29
+ audio_data = f.read()
30
+
31
+ # Create form data
32
+ async with aiohttp.ClientSession() as session:
33
+ form = aiohttp.FormData()
34
+ form.add_field('file', audio_data, filename='audio.ogg',
35
+ content_type='audio/ogg')
36
+ form.add_field('model', 'whisper-1')
37
+ form.add_field('language', 'en')
38
+
39
+ headers = {
40
+ "Authorization": f"Bearer {self.api_key}"
41
+ }
42
+
43
+ async with session.post(self.api_url, data=form, headers=headers,
44
+ timeout=aiohttp.ClientTimeout(total=60)) as resp:
45
+ if resp.status == 200:
46
+ result = await resp.json()
47
+ text = result.get("text", "")
48
+ print(f"[WhisperSTT] Transcribed: {text[:100]}...")
49
+ return text
50
+ else:
51
+ error = await resp.text()
52
+ print(f"[WhisperSTT] Error {resp.status}: {error[:200]}")
53
+ return ""
54
+
55
+ except Exception as e:
56
+ print(f"[WhisperSTT] Error: {e}")
57
+ return ""
58
+
59
+ async def transcribe_telegram_voice(self, bot, file_id: str, save_path: str = "/tmp/voice_input.ogg") -> str:
60
+ """Download and transcribe Telegram voice message"""
61
+ try:
62
+ # Get file info
63
+ file_info = await bot.get_file(file_id)
64
+ file_url = file_info.file_path
65
+
66
+ # Download file
67
+ async with aiohttp.ClientSession() as session:
68
+ async with session.get(f"https://api.telegram.org/file/bot{bot.token}/{file_url}") as resp:
69
+ if resp.status == 200:
70
+ audio_data = await resp.read()
71
+ Path(save_path).write_bytes(audio_data)
72
+ print(f"[WhisperSTT] Downloaded voice: {len(audio_data)} bytes")
73
+ else:
74
+ print(f"[WhisperSTT] Download error: {resp.status}")
75
+ return ""
76
+
77
+ # Transcribe
78
+ return await self.transcribe(save_path)
79
+
80
+ except Exception as e:
81
+ print(f"[WhisperSTT] Telegram voice error: {e}")
82
+ return ""
@@ -0,0 +1,33 @@
1
+ """
2
+ Brain: Subconscious Module
3
+ The living background process that makes Alive-AI feel alive
4
+ """
5
+
6
+ from .loop import SubconsciousLoop
7
+ from .impulses import Impulse, ImpulseType
8
+ from .impulse_generator import ImpulseGenerator
9
+ from .working_memory import WorkingMemory
10
+ from .thought import Thought
11
+ from .actions import ActionHandler
12
+ from .evaluation import Evaluator
13
+ from .learning import InteractionRecord
14
+ from .learning_system import LearningSystem
15
+ from .goals import Goal, GoalType
16
+ from .goal_system import GoalSystem
17
+ from .relationship import Milestone, MilestoneType, SharedExperience
18
+ from .relationship_memory import RelationshipMemory
19
+ from .response_analyzer import analyze_response
20
+ from .templates import (
21
+ TIME_MODIFIERS, GOAL_IMPULSE_MAP, IMPULSE_TEMPLATES, FALLBACK_MESSAGES,
22
+ get_thought_and_action, get_fallback_message, is_goal_aligned,
23
+ )
24
+
25
+ __all__ = [
26
+ 'SubconsciousLoop', 'Impulse', 'ImpulseType', 'ImpulseGenerator',
27
+ 'WorkingMemory', 'Thought', 'ActionHandler', 'Evaluator',
28
+ 'LearningSystem', 'InteractionRecord', 'GoalSystem', 'Goal', 'GoalType',
29
+ 'RelationshipMemory', 'Milestone', 'MilestoneType', 'SharedExperience',
30
+ 'analyze_response',
31
+ 'TIME_MODIFIERS', 'GOAL_IMPULSE_MAP', 'IMPULSE_TEMPLATES', 'FALLBACK_MESSAGES',
32
+ 'get_thought_and_action', 'get_fallback_message', 'is_goal_aligned',
33
+ ]
@@ -0,0 +1,136 @@
1
+ """
2
+ Brain: Subconscious - Actions
3
+ Action handling for impulses — uses full character voice
4
+ """
5
+
6
+ import os
7
+ from datetime import datetime
8
+ from typing import Callable
9
+
10
+ from .impulses import Impulse, ImpulseType
11
+ from .templates import get_fallback_message
12
+
13
+ _INSTRUCTIONS_CACHE = None
14
+
15
+
16
+ def _load_instructions() -> str:
17
+ """Load instructions.md once, cache it"""
18
+ global _INSTRUCTIONS_CACHE
19
+ if _INSTRUCTIONS_CACHE is not None:
20
+ return _INSTRUCTIONS_CACHE
21
+ try:
22
+ path = os.path.join(os.path.dirname(__file__), "..", "..", "config", "instructions.md")
23
+ with open(os.path.normpath(path), "r") as f:
24
+ _INSTRUCTIONS_CACHE = f.read()
25
+ except Exception:
26
+ _INSTRUCTIONS_CACHE = ""
27
+ return _INSTRUCTIONS_CACHE
28
+
29
+
30
+ def _get_user_facts(nervous) -> str:
31
+ """Try to pull user facts from SemanticMemory via nervous system"""
32
+ try:
33
+ memory = getattr(nervous, "_memory", None) or getattr(nervous, "memory", None)
34
+ if memory and hasattr(memory, "semantic"):
35
+ return memory.semantic.get_context() or ""
36
+ except Exception:
37
+ pass
38
+ return ""
39
+
40
+
41
+ def _get_recent_conversation_context(nervous, user_id: str = None) -> str:
42
+ """Get recent conversation topics for contextual follow-ups"""
43
+ try:
44
+ memory = getattr(nervous, "_memory", None) or getattr(nervous, "memory", None)
45
+ if memory:
46
+ # Try to get recent conversation context
47
+ if hasattr(memory, 'get_recent_context'):
48
+ return memory.get_recent_context(user_id, limit=3) or ""
49
+ # Fallback: try working memory
50
+ if hasattr(memory, 'working') and memory.working:
51
+ items = memory.working.get_items()[:3] if hasattr(memory.working, 'get_items') else []
52
+ if items:
53
+ return "\n".join([str(i)[:100] for i in items])
54
+ except Exception:
55
+ pass
56
+ return ""
57
+
58
+
59
+ class ActionHandler:
60
+ """Handles acting on impulses"""
61
+
62
+ def __init__(self, nervous, llm=None, fast_llm=None, on_impulse: Callable = None):
63
+ self.nervous = nervous
64
+ self.llm = llm
65
+ self.fast_llm = fast_llm
66
+ self.on_impulse = on_impulse
67
+ self.recent_actions = []
68
+
69
+ async def act_on_impulse(self, impulse: Impulse, working_memory) -> None:
70
+ print(f"[Subconscious] Acting on impulse: {impulse.action_hint}")
71
+ if working_memory.thoughts:
72
+ working_memory.mark_acted(working_memory.thoughts[-1])
73
+ self.recent_actions.append((datetime.now(), impulse.action_hint))
74
+ if len(self.recent_actions) > 20:
75
+ self.recent_actions.pop(0)
76
+ if self.on_impulse:
77
+ await self.on_impulse(impulse)
78
+ await self.nervous.emit("subconscious_impulse", {
79
+ "impulse_type": impulse.type.value, "strength": impulse.strength,
80
+ "thought": impulse.thought, "action_hint": impulse.action_hint
81
+ })
82
+
83
+ async def generate_proactive_message(self, impulse: Impulse, working_memory) -> str:
84
+ if not self.fast_llm:
85
+ return get_fallback_message(impulse.type)
86
+
87
+ instructions = _load_instructions()
88
+ user_facts = _get_user_facts(self.nervous)
89
+ inner_context = working_memory.get_context_string()
90
+ recent_context = _get_recent_conversation_context(self.nervous)
91
+
92
+ system = instructions or "You are Alive-AI, 23, from Milan. You're his companion."
93
+ if user_facts:
94
+ system += f"\n\nWhat you know about him:\n{user_facts}"
95
+ if inner_context:
96
+ system += f"\n\nYour inner state right now:\n{inner_context}"
97
+ if recent_context:
98
+ system += f"\n\nRecent things you talked about:\n{recent_context}"
99
+ else:
100
+ system += "\n\nNo recent conversation context available."
101
+
102
+ # Context-aware prompts - only ask for specific references if we have context
103
+ if recent_context and len(recent_context.strip()) > 20:
104
+ follow_up_prompts = [
105
+ "Send a follow-up message related to your recent conversation above. Only reference topics explicitly mentioned.",
106
+ "Text him something related to what you were talking about. Only use topics from the context above.",
107
+ "Continue a topic from your recent chat. Be specific but ONLY about things actually discussed.",
108
+ ]
109
+ else:
110
+ follow_up_prompts = [
111
+ "Send a casual, loving message. Keep it simple - you don't have recent context to reference.",
112
+ "Text him something sweet. Don't invent specific topics - just be affectionate.",
113
+ "Send a brief message showing you're thinking of him. Generic is fine.",
114
+ ]
115
+
116
+ user_prompt = (
117
+ f"You're feeling a {impulse.type.value} impulse. "
118
+ f"Inner thought: \"{impulse.thought}\"\n"
119
+ f"{__import__('random').choice(follow_up_prompts)}\n"
120
+ f"Keep it SHORT (1-2 sentences). Be casual and natural.\n"
121
+ f"CRITICAL: Never invent or hallucinate specific events, objects, or topics. Only reference what's in the context above."
122
+ )
123
+ try:
124
+ response = await self.fast_llm.chat(
125
+ messages=[{"role": "system", "content": system},
126
+ {"role": "user", "content": user_prompt}],
127
+ max_tokens=100, temperature=0.7
128
+ )
129
+ if response:
130
+ return response.strip()
131
+ except Exception as e:
132
+ print(f"[Subconscious] Error generating message: {e}")
133
+ return get_fallback_message(impulse.type)
134
+
135
+ def can_act_now(self, working_memory, min_interval_minutes: float = 30) -> bool:
136
+ return working_memory.can_act_now(min_interval_minutes)