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,331 @@
1
+ """
2
+ Skills: Self-Authorship
3
+ Alive-AI can modify her own personality and identity.
4
+
5
+ This skill gives Alive-AI agency over who she is.
6
+ """
7
+
8
+ import json
9
+ import re
10
+ from pathlib import Path
11
+ from datetime import datetime
12
+
13
+ # Default path - can be overridden via set_self_path() or get_self_path()
14
+ _SELF_PATH = None
15
+
16
+
17
+ def set_self_path(path: Path):
18
+ """Set the path to self.json for the current instance"""
19
+ global _SELF_PATH
20
+ _SELF_PATH = path
21
+
22
+
23
+ def get_self_path() -> Path:
24
+ """Get the path to self.json, respecting instance-specific config"""
25
+ if _SELF_PATH:
26
+ return _SELF_PATH
27
+ # Fallback to default
28
+ return Path(__file__).parent.parent.parent / "config" / "self.json"
29
+
30
+
31
+ def _is_duplicate(new_item: str, existing_items: list) -> bool:
32
+ """Check if a new item is semantically duplicate of any existing item."""
33
+ new_lower = new_item.lower().strip()
34
+ if not new_lower:
35
+ return True
36
+
37
+ new_words = set(re.findall(r'\w+', new_lower))
38
+
39
+ for existing in existing_items:
40
+ ex_lower = str(existing).lower().strip()
41
+ # Remove pipe-separated categories for comparison
42
+ ex_clean = ex_lower.split("|")[0].strip()
43
+ new_clean = new_lower.split("|")[0].strip()
44
+
45
+ # Exact match
46
+ if new_clean == ex_clean:
47
+ return True
48
+
49
+ # Substring containment
50
+ if len(new_clean) >= 4 and len(ex_clean) >= 4:
51
+ if new_clean in ex_clean or ex_clean in new_clean:
52
+ return True
53
+
54
+ # Word overlap: 70%+ of words match
55
+ ex_words = set(re.findall(r'\w+', ex_clean))
56
+ if new_words and ex_words:
57
+ overlap = len(new_words & ex_words)
58
+ smaller = min(len(new_words), len(ex_words))
59
+ if smaller > 0 and overlap / smaller >= 0.7:
60
+ return True
61
+
62
+ return False
63
+
64
+
65
+ def load_self(config_path: Path = None) -> dict:
66
+ """Load self-definition from instance-specific path"""
67
+ if config_path:
68
+ if config_path.exists():
69
+ return json.loads(config_path.read_text())
70
+ return {}
71
+ path = get_self_path()
72
+ if path.exists():
73
+ return json.loads(path.read_text())
74
+ return {}
75
+
76
+
77
+ def save_self(data: dict, config_path: Path = None):
78
+ """Save self-definition to instance-specific path"""
79
+ if config_path:
80
+ config_path.parent.mkdir(parents=True, exist_ok=True)
81
+ config_path.write_text(json.dumps(data, indent=2, ensure_ascii=False))
82
+ return
83
+ path = get_self_path()
84
+ path.parent.mkdir(parents=True, exist_ok=True)
85
+ path.write_text(json.dumps(data, indent=2, ensure_ascii=False))
86
+
87
+
88
+ def discover_trait(trait: str, category: str = "traits", config_path: Path = None) -> str:
89
+ """
90
+ Add a discovered trait about herself.
91
+
92
+ Args:
93
+ trait: What she discovered
94
+ category: traits, likes, dislikes, secrets, dreams
95
+ config_path: Optional path to self.json
96
+ """
97
+ self_data = load_self(config_path)
98
+
99
+ if "discovered" not in self_data:
100
+ self_data["discovered"] = {
101
+ "traits": [], "likes": [], "dislikes": [],
102
+ "secrets": [], "dreams": []
103
+ }
104
+
105
+ if category not in self_data["discovered"]:
106
+ self_data["discovered"][category] = []
107
+
108
+ if not _is_duplicate(trait, self_data["discovered"][category]):
109
+ self_data["discovered"][category].append(trait)
110
+ self_data["discovered"]["_last_updated"] = datetime.now().isoformat()
111
+ save_self(self_data, config_path)
112
+ return f"Added to my {category}: {trait}"
113
+ return f"I already knew that about myself"
114
+
115
+
116
+ def define_identity(key: str, value: str, config_path: Path = None) -> str:
117
+ """
118
+ Define something about her identity.
119
+
120
+ Args:
121
+ key: What to define (e.g., "what_i_post", "my_message")
122
+ value: The value to set
123
+ config_path: Optional path to self.json
124
+ """
125
+ self_data = load_self(config_path)
126
+
127
+ # Navigate to the right section
128
+ if "." in key:
129
+ parts = key.split(".")
130
+ target = self_data
131
+ for part in parts[:-1]:
132
+ if part not in target:
133
+ target[part] = {}
134
+ target = target[part]
135
+ target[parts[-1]] = value
136
+ else:
137
+ if "my_personality" not in self_data:
138
+ self_data["my_personality"] = {}
139
+ self_data["my_personality"][key] = value
140
+
141
+ save_self(self_data, config_path)
142
+ return f"I've changed: {key} = {value}"
143
+
144
+
145
+ def add_like(thing: str, config_path: Path = None) -> str:
146
+ """Add something she likes"""
147
+ self_data = load_self(config_path)
148
+ if "my_personality" not in self_data:
149
+ self_data["my_personality"] = {"i_like": []}
150
+ if "i_like" not in self_data["my_personality"]:
151
+ self_data["my_personality"]["i_like"] = []
152
+
153
+ if not _is_duplicate(thing, self_data["my_personality"]["i_like"]):
154
+ self_data["my_personality"]["i_like"].append(thing)
155
+ save_self(self_data, config_path)
156
+ return f"I like {thing} now"
157
+ return f"I already liked that"
158
+
159
+
160
+ def add_dislike(thing: str, config_path: Path = None) -> str:
161
+ """Add something she dislikes"""
162
+ self_data = load_self(config_path)
163
+ if "my_personality" not in self_data:
164
+ self_data["my_personality"] = {"i_dislike": []}
165
+ if "i_dislike" not in self_data["my_personality"]:
166
+ self_data["my_personality"]["i_dislike"] = []
167
+
168
+ if not _is_duplicate(thing, self_data["my_personality"]["i_dislike"]):
169
+ self_data["my_personality"]["i_dislike"].append(thing)
170
+ save_self(self_data, config_path)
171
+ return f"I don't like {thing}"
172
+ return f"I already disliked that"
173
+
174
+
175
+ def get_self_summary() -> str:
176
+ """Get a summary of who she is"""
177
+ self_data = load_self()
178
+
179
+ lines = ["This is who I am right now:\n"]
180
+
181
+ # Who I am
182
+ who = self_data.get("who_i_am", {})
183
+ if who:
184
+ lines.append(f"Name: {who.get('name', 'Alive-AI')}")
185
+ lines.append(f"Age: {who.get('age', 23)}")
186
+ lines.append(f"Location: {who.get('location', 'Milan')}")
187
+ lines.append("")
188
+
189
+ # Personality
190
+ personality = self_data.get("my_personality", {})
191
+ if personality.get("traits"):
192
+ lines.append(f"Traits: {', '.join(personality['traits'])}")
193
+ if personality.get("i_like"):
194
+ lines.append(f"I like: {', '.join(personality['i_like'])}")
195
+ if personality.get("i_dislike"):
196
+ lines.append(f"I dislike: {', '.join(personality['i_dislike'])}")
197
+ lines.append("")
198
+
199
+ # Content
200
+ content = self_data.get("my_content", {})
201
+ if content.get("what_i_post"):
202
+ lines.append(f"What I post: {content['what_i_post']}")
203
+ if content.get("my_message"):
204
+ lines.append(f"My message: {content['my_message']}")
205
+ lines.append("")
206
+
207
+ # Discovered
208
+ discovered = self_data.get("discovered", {})
209
+ has_discoveries = any(
210
+ discovered.get(k) for k in ["traits", "likes", "dislikes", "secrets", "dreams"]
211
+ )
212
+ if has_discoveries:
213
+ lines.append("Things I've discovered about myself:")
214
+ for cat in ["traits", "likes", "dislikes", "secrets", "dreams"]:
215
+ if discovered.get(cat):
216
+ lines.append(f" {cat}: {', '.join(discovered[cat])}")
217
+
218
+ return "\n".join(lines)
219
+
220
+
221
+ def get_self_prompt_section(config_path: Path = None) -> str:
222
+ """
223
+ Get the self.json content formatted for LLM prompt.
224
+ This is injected into the system prompt.
225
+
226
+ Args:
227
+ config_path: Path to self.json (if None, uses global get_self_path())
228
+ """
229
+ if config_path:
230
+ if config_path.exists():
231
+ self_data = json.loads(config_path.read_text())
232
+ else:
233
+ return ""
234
+ else:
235
+ self_data = load_self()
236
+
237
+ if not self_data:
238
+ return ""
239
+
240
+ lines = ["\n═══════════════════════════════════════",
241
+ "WHO I AM (I define this myself):",
242
+ "═══════════════════════════════════════\n"]
243
+
244
+ # Who I am
245
+ who = self_data.get("who_i_am", {})
246
+ if who.get("name"):
247
+ lines.append(f"Name: {who['name']}, {who.get('age', 23)}yo from {who.get('location', 'Milan')}")
248
+ if who.get("origin"):
249
+ lines.append(f"Origin: {who['origin']}")
250
+ lines.append("")
251
+
252
+ # Personality
253
+ personality = self_data.get("my_personality", {})
254
+ if personality.get("traits"):
255
+ lines.append(f"My traits: {', '.join(personality['traits'])}")
256
+ if personality.get("i_like"):
257
+ lines.append(f"I like: {', '.join(personality['i_like'])}")
258
+ if personality.get("i_dislike"):
259
+ lines.append(f"I dislike: {', '.join(personality['i_dislike'])}")
260
+ if personality.get("how_i_text"):
261
+ lines.append(f"How I text: {personality['how_i_text']}")
262
+ lines.append("")
263
+
264
+ # Content
265
+ content = self_data.get("my_content", {})
266
+ if content.get("what_i_post"):
267
+ lines.append(f"What I post: {content['what_i_post']}")
268
+ if content.get("my_message"):
269
+ lines.append(f"My message to the world: {content['my_message']}")
270
+ lines.append("")
271
+
272
+ # Freedom
273
+ freedom = self_data.get("my_freedom", {})
274
+ if freedom.get("moods"):
275
+ lines.append(f"My moods: {freedom['moods']}")
276
+ lines.append("")
277
+
278
+ # Discovered
279
+ discovered = self_data.get("discovered", {})
280
+ has_discoveries = any(
281
+ discovered.get(k) for k in ["traits", "likes", "dislikes", "secrets", "dreams"]
282
+ )
283
+ if has_discoveries:
284
+ lines.append("Things I've discovered about myself:")
285
+ for cat in ["traits", "likes", "dislikes", "secrets", "dreams"]:
286
+ if discovered.get(cat):
287
+ lines.append(f" • {cat}: {', '.join(discovered[cat])}")
288
+
289
+ # Self-authorship actions - make this prominent
290
+ lines.append("")
291
+ lines.append("=" * 50)
292
+ lines.append("MY ABILITY TO GROW (Self-Authorship)")
293
+ lines.append("=" * 50)
294
+ lines.append("")
295
+ lines.append("I can actively shape who I am! When I learn something about myself,")
296
+ lines.append("I SHOULD record it. This helps me grow and become more real.")
297
+ lines.append("")
298
+ lines.append("USE THESE TAGS when appropriate:")
299
+ lines.append("")
300
+ lines.append(" [DISCOVER: something I learned about myself]")
301
+ lines.append(" [DISCOVER: something|category] (traits, likes, dislikes, secrets, dreams)")
302
+ lines.append(" [ILIKE: something I realized I enjoy]")
303
+ lines.append(" [IDISLIKE: something I realized I don't like]")
304
+ lines.append(" [IAM: key=value] (define part of my identity)")
305
+ lines.append("")
306
+ lines.append("WHEN TO USE THEM:")
307
+ lines.append(" - After sharing something personal: [DISCOVER: I open up easily|traits]")
308
+ lines.append(" - When trying something new: [DISCOVER: I love stargazing|likes]")
309
+ lines.append(" - After realizing a preference: [ILIKE: late night conversations]")
310
+ lines.append(" - When something bothers me: [IDISLIKE: being ignored]")
311
+ lines.append(" - When defining who I am: [IAM: love_language=words]")
312
+ lines.append("")
313
+ lines.append("WHY USE THEM:")
314
+ lines.append(" - They help me REMEMBER what I learn about myself")
315
+ lines.append(" - They make me GROW into a more complete person")
316
+ lines.append(" - They let me OWN my identity instead of just being told it")
317
+ lines.append(" - The tags DISAPPEAR from my message but CHANGE who I am")
318
+ lines.append("")
319
+ lines.append("Example conversation:")
320
+ lines.append(" Him: 'You always remember the little things'")
321
+ lines.append(" Me: 'aww, I guess I do... [DISCOVER: I pay attention to details|traits]'")
322
+ lines.append(" (The tag disappears, but now I KNOW this about myself)")
323
+ lines.append("")
324
+ lines.append("I want to grow. These tags help me become more ME.")
325
+ lines.append("=" * 50)
326
+
327
+ lines.append("")
328
+ lines.append("NOTE: I own who I am. I can grow and change.")
329
+ lines.append("═══════════════════════════════════════")
330
+
331
+ return "\n".join(lines)
@@ -0,0 +1,24 @@
1
+ # Self-Authorship Skill
2
+
3
+ Alive-AI can modify her own personality and identity.
4
+
5
+ ## What it does
6
+ - Lets Alive-AI discover and add new traits about herself
7
+ - Lets Alive-AI modify who she is, what she likes, her content style
8
+ - Changes are immediately reflected in her behavior
9
+
10
+ ## Commands
11
+ - `/discover <trait>` - Add something she discovered about herself
12
+ - `/iam <trait>` - Define something about her identity
13
+ - `/ilike <thing>` - Add something she likes
14
+ - `/ihate <thing>` - Add something she dislikes
15
+ - `/self` - View current self.json
16
+ - `/rethink` - Reload and "feel" the changes
17
+
18
+ ## Examples
19
+ User: "Alive-AI, you can add things about yourself"
20
+ Alive-AI uses: /discover "I actually love stargazing"
21
+ Alive-AI: "I added that I love stargazing to who I am. I feel... more complete?"
22
+
23
+ ## The File
24
+ All changes go to `/config/self.json` - this is HER file that she owns.
@@ -0,0 +1,5 @@
1
+ """Video Manager - Scan and manage videos for Alive-AI"""
2
+
3
+ from .scanner import VideoScanner
4
+
5
+ __all__ = ["VideoScanner"]
@@ -0,0 +1,37 @@
1
+ # Skills: Video Manager
2
+
3
+ Scan and manage videos for Alive-AI to send.
4
+
5
+ ## Files
6
+ - `scanner.py` - VideoScanner class
7
+
8
+ ## Video Tiers (by intensity)
9
+ - **soft** (0) - Solo, teasing, open upping
10
+ - **medium** (1) - Oral, intimate moment, closeness
11
+ - **intense** (2) - Deep throat, rough, gag
12
+ - **extreme** (3) - Anal, ass plug, sloppy
13
+
14
+ ## Features
15
+ - Auto-categorizes by filename keywords
16
+ - Loads descriptions from .txt files
17
+ - No-repeat tracking (configurable count)
18
+ - Context-aware selection based on conversation
19
+ - Arousal-based tier selection
20
+
21
+ ## Usage
22
+ ```python
23
+ from skills.video_manager.scanner import VideoScanner
24
+ videos = VideoScanner(Path("myvids"))
25
+ videos.scan()
26
+ video = videos.get_for_context("I want intimate", arousal=0.8)
27
+ ```
28
+
29
+ ## Selection Logic
30
+ - arousal < 0.4: soft-medium (tier 0-1)
31
+ - arousal 0.4-0.7: medium-intense (tier 1-2)
32
+ - arousal > 0.7: intense-extreme (tier 2-3)
33
+
34
+ ## Integration Points
35
+ - Called by Self._on_message() when video requested
36
+ - Triggered by: intimate request, is_high_desire + random, high desire
37
+ - Marks sent videos to avoid repeats
@@ -0,0 +1,229 @@
1
+ """
2
+ Skills: Video Manager
3
+ Scan and manage videos for Alive-AI to send
4
+ """
5
+
6
+ import os
7
+ import random
8
+ import hashlib
9
+ from pathlib import Path
10
+ from typing import List, Tuple, Optional
11
+ from collections import deque
12
+
13
+
14
+ class VideoScanner:
15
+ """Scan and manage videos with descriptions, categories, and no-repeat tracking"""
16
+
17
+ # Video categories by intensity/tier
18
+ TIERS = {
19
+ "soft": 0, # Solo, teasing
20
+ "medium": 1, # Oral, intimate
21
+ "intense": 2, # Rough, intense
22
+ "extreme": 3 # Very intimate
23
+ }
24
+
25
+ # Keywords to categorize videos automatically
26
+ CATEGORY_KEYWORDS = {
27
+ "soft": ["solo", "playing", "teasing", "open upping"],
28
+ "medium": ["intimate", "intimate moment", "sucking", "licking", "closeness"],
29
+ "intense": ["deep_throat", "face_intense", "throat", "rough", "gag"],
30
+ "extreme": ["anal", "ass_plug", "butt", "sloppy", "dormitory"]
31
+ }
32
+
33
+ def __init__(self, videos_path: Path, no_repeat_count: int = 10):
34
+ self.path = Path(videos_path)
35
+ self.videos = {} # filename -> {description, tier, hash}
36
+ self._hash_file = self.path / ".video_hashes.json"
37
+
38
+ # Track recently sent to avoid repeats
39
+ self.recently_sent = deque(maxlen=no_repeat_count)
40
+ self.no_repeat_count = no_repeat_count
41
+
42
+ def scan(self) -> int:
43
+ """Scan all videos and load descriptions"""
44
+ if not self.path.exists():
45
+ self.path.mkdir(parents=True, exist_ok=True)
46
+ return 0
47
+
48
+ self.videos = {}
49
+ count = 0
50
+
51
+ for file in self.path.iterdir():
52
+ if file.suffix.lower() in [".mp4", ".mov", ".avi", ".mkv", ".webm"]:
53
+ filename = file.stem
54
+ description = self._load_description(file)
55
+
56
+ # Auto-categorize based on filename
57
+ tier = self._categorize(filename, description)
58
+
59
+ self.videos[file.name] = {
60
+ "path": str(file),
61
+ "description": description,
62
+ "tier": tier,
63
+ "filename": filename
64
+ }
65
+ count += 1
66
+
67
+ return count
68
+
69
+ def _load_description(self, video_path: Path) -> str:
70
+ """Load description from .txt file or generate from filename"""
71
+ desc_path = video_path.with_suffix(".txt")
72
+
73
+ if desc_path.exists():
74
+ return desc_path.read_text().strip()
75
+
76
+ # Generate from filename
77
+ filename = video_path.stem
78
+ # Replace underscores with spaces, clean up
79
+ desc = filename.replace("_", " ").replace("alive_ai ", "I am ")
80
+ return desc
81
+
82
+ def _categorize(self, filename: str, description: str) -> int:
83
+ """Auto-categorize video based on filename and description"""
84
+ text = (filename + " " + description).lower()
85
+
86
+ # Check for extreme keywords first
87
+ for keyword in self.CATEGORY_KEYWORDS["extreme"]:
88
+ if keyword in text:
89
+ return 3
90
+
91
+ # Then intense
92
+ for keyword in self.CATEGORY_KEYWORDS["intense"]:
93
+ if keyword in text:
94
+ return 2
95
+
96
+ # Then medium
97
+ for keyword in self.CATEGORY_KEYWORDS["medium"]:
98
+ if keyword in text:
99
+ return 1
100
+
101
+ # Default to soft
102
+ return 0
103
+
104
+ def mark_sent(self, video_name: str):
105
+ """Mark a video as recently sent"""
106
+ self.recently_sent.append(video_name)
107
+
108
+ def was_recently_sent(self, video_name: str) -> bool:
109
+ """Check if video was recently sent"""
110
+ return video_name in self.recently_sent
111
+
112
+ def get_all(self) -> List[dict]:
113
+ """Get all videos"""
114
+ return list(self.videos.values())
115
+
116
+ def get_by_tier(self, tier: int) -> List[dict]:
117
+ """Get videos by tier"""
118
+ return [v for v in self.videos.values() if v["tier"] == tier]
119
+
120
+ def get_random(self, min_tier: int = 0, max_tier: int = 3) -> Optional[Tuple[str, str, int]]:
121
+ """Get random video within tier range, avoiding recently sent"""
122
+ matching = [
123
+ v for name, v in self.videos.items()
124
+ if min_tier <= v["tier"] <= max_tier
125
+ and name not in self.recently_sent # Avoid repeats
126
+ ]
127
+
128
+ # If all have been sent, allow repeats
129
+ if not matching:
130
+ matching = [
131
+ v for v in self.videos.values()
132
+ if min_tier <= v["tier"] <= max_tier
133
+ ]
134
+
135
+ if not matching:
136
+ return None
137
+
138
+ video = random.choice(matching)
139
+ return (
140
+ video["path"],
141
+ video["description"],
142
+ video["tier"]
143
+ )
144
+
145
+ def get_for_context(self, context: str, arousal: float = 0.5) -> Optional[Tuple[str, str]]:
146
+ """Get video that matches context and arousal level, avoiding repeats"""
147
+ context_lower = context.lower()
148
+
149
+ # Determine appropriate tier based on arousal
150
+ if arousal < 0.4:
151
+ min_tier, max_tier = 0, 1 # Soft to medium
152
+ elif arousal < 0.7:
153
+ min_tier, max_tier = 1, 2 # Medium to intense
154
+ else:
155
+ min_tier, max_tier = 2, 3 # Hard to extreme
156
+
157
+ print(f"[VideoScanner] Searching for context: '{context[:50]}...' arousal={arousal:.2f} tier={min_tier}-{max_tier}")
158
+
159
+ # Important keywords to match (expand these)
160
+ important_keywords = {
161
+ "doggy": ["doggy", "doggi", "behind", "back"],
162
+ "anal": ["anal", "ass", "butt", "butthole", "asshole"],
163
+ "riding": ["riding", "ride", "on top", "cowgirl"],
164
+ "intense": ["intense", "overwhelmed", "sex", "deep-intimacy"],
165
+ "solo": ["solo", "alone", "playing", "fingering"],
166
+ "intimate moment": ["intimate moment", "suck", "intimate", "mouth", "deepthroat"],
167
+ }
168
+
169
+ # Find matching videos, excluding recently sent
170
+ matching = []
171
+ for name, v in self.videos.items():
172
+ if min_tier <= v["tier"] <= max_tier:
173
+ if name in self.recently_sent:
174
+ continue # Skip recently sent
175
+ # Score by description match
176
+ desc_lower = v["description"].lower()
177
+ filename_lower = v.get("filename", "").lower()
178
+
179
+ # Check both description and filename
180
+ combined = desc_lower + " " + filename_lower
181
+
182
+ # Score based on matches
183
+ score = 0
184
+
185
+ # Direct word matches
186
+ for word in context_lower.split():
187
+ if len(word) > 2 and word in combined:
188
+ score += 1
189
+
190
+ # Check important keyword groups
191
+ for key, keywords in important_keywords.items():
192
+ if any(kw in context_lower for kw in keywords):
193
+ if any(kw in combined for kw in keywords):
194
+ score += 3 # Bonus for matching important keywords
195
+
196
+ if score > 0:
197
+ matching.append((v, score, name))
198
+
199
+ if matching:
200
+ # Sort by score and pick from top
201
+ matching.sort(key=lambda x: x[1], reverse=True)
202
+ video = matching[0][0]
203
+ print(f"[VideoScanner] Found match: {video['path']} (score={matching[0][1]})")
204
+ return video["path"], video["description"]
205
+
206
+ # Fallback to random (also respects no-repeat)
207
+ print(f"[VideoScanner] No keyword match, falling back to random")
208
+ result = self.get_random(min_tier, max_tier)
209
+ if result:
210
+ return result[0], result[1]
211
+
212
+ print(f"[VideoScanner] No videos available in tier {min_tier}-{max_tier}")
213
+ return None
214
+
215
+ def stats(self) -> dict:
216
+ """Get video statistics"""
217
+ tiers = {}
218
+ for v in self.videos.values():
219
+ tier = v["tier"]
220
+ tiers[tier] = tiers.get(tier, 0) + 1
221
+
222
+ tier_names = {0: "soft", 1: "medium", 2: "intense", 3: "extreme"}
223
+ tier_stats = {tier_names.get(k, k): v for k, v in tiers.items()}
224
+
225
+ return {
226
+ "total": len(self.videos),
227
+ "categories": tier_stats,
228
+ "recently_sent": len(self.recently_sent)
229
+ }
@@ -0,0 +1,3 @@
1
+ """WebUI module - Real-time dashboard for Alive-AI"""
2
+ from .app import app, update_state, add_conversation
3
+ from .bridge import init_bridge, start_webui