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.
- package/Dockerfile +24 -0
- package/LICENSE +21 -0
- package/README.md +143 -0
- package/alive_ai/__init__.py +3 -0
- package/brain/__init__.py +59 -0
- package/brain/almost_said.py +154 -0
- package/brain/bid_detector.py +636 -0
- package/brain/conversation_flow.py +135 -0
- package/brain/curiosity.py +328 -0
- package/brain/default_mode.py +1438 -0
- package/brain/dreams.py +220 -0
- package/brain/embeddings/__init__.py +82 -0
- package/brain/emotional_memory.py +949 -0
- package/brain/global_activity.py +173 -0
- package/brain/group_dynamics.py +63 -0
- package/brain/linguistic.py +235 -0
- package/brain/llm/__init__.py +63 -0
- package/brain/llm/base.py +33 -0
- package/brain/llm/fallback_router.py +309 -0
- package/brain/llm/manifest.md +30 -0
- package/brain/llm/ollama.py +218 -0
- package/brain/llm/openrouter.py +151 -0
- package/brain/llm/provider.py +205 -0
- package/brain/llm/unified.py +423 -0
- package/brain/llm/zai.py +169 -0
- package/brain/manifest.md +23 -0
- package/brain/memory/__init__.py +123 -0
- package/brain/memory/episodic.py +92 -0
- package/brain/memory/fact_extractor.py +209 -0
- package/brain/memory/index.py +54 -0
- package/brain/memory/manager.py +151 -0
- package/brain/memory/summarizer.py +102 -0
- package/brain/memory/vector_store.py +297 -0
- package/brain/memory/working.py +43 -0
- package/brain/narrative.py +343 -0
- package/brain/stt/__init__.py +4 -0
- package/brain/stt/google_stt.py +83 -0
- package/brain/stt/whisper_stt.py +82 -0
- package/brain/subconscious/__init__.py +33 -0
- package/brain/subconscious/actions.py +136 -0
- package/brain/subconscious/evaluation.py +166 -0
- package/brain/subconscious/goal_system.py +90 -0
- package/brain/subconscious/goals.py +41 -0
- package/brain/subconscious/impulse_generator.py +200 -0
- package/brain/subconscious/impulses.py +48 -0
- package/brain/subconscious/learning.py +24 -0
- package/brain/subconscious/learning_system.py +79 -0
- package/brain/subconscious/loop.py +398 -0
- package/brain/subconscious/manifest.md +32 -0
- package/brain/subconscious/relationship.py +47 -0
- package/brain/subconscious/relationship_memory.py +83 -0
- package/brain/subconscious/response_analyzer.py +74 -0
- package/brain/subconscious/templates.py +70 -0
- package/brain/subconscious/thought.py +37 -0
- package/brain/subconscious/working_memory.py +97 -0
- package/cli/index.js +371 -0
- package/config/directives.example.json +28 -0
- package/config/instructions.example.md +16 -0
- package/config/self.example.json +74 -0
- package/config/settings.example.json +95 -0
- package/core/__init__.py +1 -0
- package/core/config.py +54 -0
- package/core/directives.py +198 -0
- package/core/events.py +50 -0
- package/core/follow_up.py +267 -0
- package/core/hot_reload.py +174 -0
- package/core/initialization.py +253 -0
- package/core/manifest.md +28 -0
- package/core/media_handler.py +241 -0
- package/core/memory_monitor.py +200 -0
- package/core/message_handler.py +1440 -0
- package/core/proactive_generator.py +277 -0
- package/core/self.py +188 -0
- package/core/settings.py +169 -0
- package/core/skills_registry.py +357 -0
- package/core/state.py +27 -0
- package/core/subconscious_bridge.py +93 -0
- package/core/thinking.py +175 -0
- package/core/user_manager.py +306 -0
- package/core/user_tracker.py +144 -0
- package/demo/index.html +144 -0
- package/docker-compose.yml +28 -0
- package/docs/assets/logo.svg +15 -0
- package/docs/index.html +355 -0
- package/heart/__init__.py +93 -0
- package/heart/afterglow.py +215 -0
- package/heart/attachment.py +186 -0
- package/heart/circadian.py +251 -0
- package/heart/complex_emotions.py +114 -0
- package/heart/conflicts.py +589 -0
- package/heart/core.py +387 -0
- package/heart/emotional_decay.py +59 -0
- package/heart/emotional_memory.py +261 -0
- package/heart/emotional_state.py +146 -0
- package/heart/emotional_variability.py +156 -0
- package/heart/hormonal.py +424 -0
- package/heart/inconsistency.py +1222 -0
- package/heart/integrity.py +469 -0
- package/heart/interoception.py +997 -0
- package/heart/love.py +120 -0
- package/heart/manifest.md +25 -0
- package/heart/mood_shifts.py +169 -0
- package/heart/phantom_somatic.py +259 -0
- package/heart/predictive.py +374 -0
- package/heart/scars.py +474 -0
- package/heart/somatic.py +482 -0
- package/heart/soul.py +633 -0
- package/heart/telemetry.py +942 -0
- package/heart/triggers.py +119 -0
- package/heart/unconscious.py +443 -0
- package/input/__init__.py +1 -0
- package/input/manifest.md +24 -0
- package/input/telegram/__init__.py +1 -0
- package/input/telegram/commands.py +762 -0
- package/input/telegram/listener.py +532 -0
- package/main.py +90 -0
- package/manifest.md +28 -0
- package/mypics/.gitkeep +1 -0
- package/myvids/.gitkeep +1 -0
- package/output/__init__.py +1 -0
- package/output/images/__init__.py +1 -0
- package/output/images/fal_gen.py +43 -0
- package/output/manifest.md +26 -0
- package/output/text/__init__.py +1 -0
- package/output/text/sender.py +22 -0
- package/output/voice/__init__.py +64 -0
- package/output/voice/google_tts.py +252 -0
- package/output/voice/gtts_tts.py +214 -0
- package/output/voice/vibe_tts.py +190 -0
- package/package.json +58 -0
- package/pyproject.toml +23 -0
- package/requirements.txt +21 -0
- package/skills/__init__.py +1 -0
- package/skills/anticipation_engine/__init__.py +8 -0
- package/skills/anticipation_engine/engine.py +618 -0
- package/skills/anticipation_engine/manifest.md +192 -0
- package/skills/calendar/__init__.py +1 -0
- package/skills/content_unlocks/__init__.py +8 -0
- package/skills/content_unlocks/manifest.md +231 -0
- package/skills/content_unlocks/unlocks.py +945 -0
- package/skills/exclusive_moments/__init__.py +8 -0
- package/skills/exclusive_moments/manifest.md +145 -0
- package/skills/exclusive_moments/moments.py +506 -0
- package/skills/intimacy_layers/__init__.py +8 -0
- package/skills/intimacy_layers/layers.py +703 -0
- package/skills/intimacy_layers/manifest.md +203 -0
- package/skills/manifest.md +67 -0
- package/skills/memory_callbacks/__init__.py +9 -0
- package/skills/memory_callbacks/callbacks.py +748 -0
- package/skills/memory_callbacks/manifest.md +170 -0
- package/skills/message_scheduler/__init__.py +19 -0
- package/skills/message_scheduler/manifest.md +107 -0
- package/skills/message_scheduler/scheduler.py +510 -0
- package/skills/photo_manager/__init__.py +1 -0
- package/skills/photo_manager/scanner.py +296 -0
- package/skills/relationship_milestones/__init__.py +8 -0
- package/skills/relationship_milestones/manifest.md +206 -0
- package/skills/relationship_milestones/tracker.py +494 -0
- package/skills/self_authorship/__init__.py +23 -0
- package/skills/self_authorship/author.py +331 -0
- package/skills/self_authorship/manifest.md +24 -0
- package/skills/video_manager/__init__.py +5 -0
- package/skills/video_manager/manifest.md +37 -0
- package/skills/video_manager/scanner.py +229 -0
- package/webui/__init__.py +3 -0
- package/webui/app.py +936 -0
- package/webui/bridge.py +366 -0
- 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,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
|
+
}
|