| Server IP : 157.230.181.24 / Your IP : 216.73.217.11 Web Server : Apache/2.4.58 (Ubuntu) System : Linux conductive 6.8.0-117-generic #117-Ubuntu SMP PREEMPT_DYNAMIC Tue May 5 19:26:24 UTC 2026 x86_64 User : ( 1000) PHP Version : 8.3.31 Disable Function : NONE MySQL : OFF | cURL : ON | WGET : ON | Perl : ON | Python : OFF | Sudo : ON | Pkexec : OFF Directory : /proc/self/root/opt/aim-bots/ |
Upload File : |
#!/usr/bin/env python3
"""Extract reply-delay and burst timing from AIM log timestamps into voice.json."""
from __future__ import annotations
import argparse
import json
import random
import re
import statistics
import subprocess
from datetime import datetime
from pathlib import Path
DB = {
"user": "convo",
"password": "b9db74bfda4d4e73",
"database": "convo",
}
CONAN_ID = 10
PARTNER_KEY = "conan0fthenight"
SPEAKER_RE = re.compile(
r">(?:Auto response from )?\s*([^<\n]+?)<!--\s*\(([^)]+)\)\s*-->",
re.I,
)
TEXT_RE = re.compile(r"<FONT[^>]*>([^<]{1,220})</FONT>", re.I)
PERSONA_ALIASES = {
"magus5311": ("magus5311", "magusishiding", "Magus5311", "MagusisHiding"),
"soewaslike": ("soewaslike", "soEwaslike", "so E was like"),
}
SIDE_ALIASES = {
"kookies4katelyn": ("Katelyn",),
"burnoutonme": ("Burn out on me",),
"leetlegirllush": ("leetle girl lush",),
"ohtaylorrr": ("Oh Taylorrr",),
"gioiamichelle": ("GioiA MicheLLe",),
"teachanx3": ("Tea Chan x3",),
"scenebarbieex": ("scene barbiee x",),
"teasememarissa": ("Tease ME Marissa",),
"alllipsofredred": ("alllipsof redred",),
"dudesy7777": ("dudesy7777", "Auto response from dudesy7777"),
}
def norm(name: str) -> str:
return re.sub(r"\s+", "", name.lower())
def mysql_rows(sql: str) -> list[str]:
return subprocess.check_output(
["mysql", "-u", DB["user"], f"-p{DB['password']}", DB["database"], "-N", "-B", "-e", sql],
text=True,
errors="replace",
).splitlines()
def mysql_content(sql: str) -> str:
"""Return full query result — AIM logs embed newlines inside content rows."""
return subprocess.check_output(
["mysql", "-u", DB["user"], f"-p{DB['password']}", DB["database"], "-N", "-B", "-e", sql],
text=True,
errors="replace",
).rstrip("\n")
def parse_time(raw: str) -> datetime | None:
raw = raw.strip()
for fmt in ("%I:%M:%S %p", "%I:%M %p", "%H:%M:%S", "%H:%M"):
try:
return datetime.strptime(raw, fmt)
except ValueError:
continue
return None
def delta_seconds(a: datetime, b: datetime) -> float:
d = (b - a).total_seconds()
if d < 0:
d += 24 * 3600
return d
def strip_msg(chunk: str) -> str:
texts = [t.strip() for t in TEXT_RE.findall(chunk) if t.strip() and t.strip() != ":"]
if not texts:
return ""
text = texts[0]
text = re.sub(r"\s+", " ", text).strip()
if re.search(r"signed (on|off)|is away|is idle|direct connection", text, re.I):
return ""
return text
def classify_side(name: str, persona_key: str, display: str, aliases: tuple[str, ...]) -> str | None:
n = norm(name)
if "conan" in n:
return PARTNER_KEY
pk = norm(persona_key)
disp = norm(display)
if n == pk or n == disp:
return persona_key
for alias in aliases:
if n == norm(alias):
return persona_key
if pk in n or n in pk:
return persona_key
# 1:1 archive convo — any non-Conan speaker is the persona
return persona_key
def parse_entries(content: str, persona_key: str, display: str, aliases: tuple[str, ...]) -> list[dict]:
entries = []
matches = list(SPEAKER_RE.finditer(content))
for i, match in enumerate(matches):
side = classify_side(match.group(1).strip(), persona_key, display, aliases)
if not side:
continue
ts = parse_time(match.group(2))
if not ts:
continue
start = match.end()
end = matches[i + 1].start() if i + 1 < len(matches) else len(content)
text = strip_msg(content[start:end])
entries.append({"side": side, "ts": ts, "text": text, "words": len(text.split()) if text else 0})
return entries
def percentile(values: list[float], pct: float) -> float:
if not values:
return 0.0
values = sorted(values)
idx = min(len(values) - 1, int(len(values) * pct / 100))
return round(values[idx], 2)
def subsample(values: list[float], cap: int = 400) -> list[float]:
if len(values) <= cap:
return [round(v, 2) for v in values]
random.seed(42)
return [round(v, 2) for v in random.sample(values, cap)]
def analyze_conversation(entries: list[dict], persona_key: str) -> dict:
reply_gaps: list[float] = []
hot_gaps: list[float] = []
warm_gaps: list[float] = []
cold_gaps: list[float] = []
burst_gaps: list[float] = []
hesitation: list[float] = []
burst_examples: list[dict] = []
for i in range(1, len(entries)):
prev, cur = entries[i - 1], entries[i]
gap = delta_seconds(prev["ts"], cur["ts"])
if not (0 < gap < 900):
continue
if prev["side"] == PARTNER_KEY and cur["side"] == persona_key:
reply_gaps.append(gap)
hot = False
if i >= 2 and entries[i - 2]["side"] == persona_key:
back = delta_seconds(entries[i - 2]["ts"], prev["ts"])
hot = back < 90
if hot:
hot_gaps.append(gap)
elif gap >= 90:
cold_gaps.append(gap)
else:
warm_gaps.append(gap)
if gap >= 45 and cur["words"] <= 4:
hesitation.append(gap)
if prev["side"] == persona_key and cur["side"] == persona_key:
burst_gaps.append(gap)
i = 0
while i < len(entries):
if entries[i]["side"] != persona_key:
i += 1
continue
burst = [entries[i]]
j = i + 1
while j < len(entries) and entries[j]["side"] == persona_key:
burst.append(entries[j])
j += 1
if len(burst) >= 2:
msgs = [b["text"] for b in burst if b["text"]]
if msgs:
gaps = [delta_seconds(burst[k - 1]["ts"], burst[k]["ts"]) for k in range(1, len(burst))]
burst_examples.append({"messages": msgs[:4], "gaps_sec": [round(g, 1) for g in gaps[:3]]})
i = j
return {
"reply_gaps": reply_gaps,
"hot_gaps": hot_gaps,
"warm_gaps": warm_gaps,
"cold_gaps": cold_gaps,
"burst_gaps": burst_gaps,
"hesitation": hesitation,
"burst_examples": burst_examples,
}
def lookup_account_ids(persona_key: str, display: str) -> list[int]:
names = list(PERSONA_ALIASES.get(persona_key, (persona_key, display)))
names.extend(SIDE_ALIASES.get(persona_key, ()))
ids = []
for name in names:
q = (
f"SELECT id FROM accounts WHERE LOWER(REPLACE(screenname,' ',''))="
f"LOWER(REPLACE('{name.replace(chr(39), chr(39)*2)}',' ','')) LIMIT 1"
)
out = mysql_rows(q)
if out:
ids.append(int(out[0]))
if not ids:
q = (
f"SELECT id FROM accounts WHERE LOWER(REPLACE(screenname,' ',''))="
f"LOWER(REPLACE('{persona_key}',' ','')) LIMIT 1"
)
out = mysql_rows(q)
if out:
ids.append(int(out[0]))
return list(dict.fromkeys(ids))
def build_profile(all_stats: list[dict]) -> dict:
reply = [g for s in all_stats for g in s["reply_gaps"]]
hot = [g for s in all_stats for g in s["hot_gaps"]]
warm = [g for s in all_stats for g in s["warm_gaps"]]
cold = [g for s in all_stats for g in s["cold_gaps"]]
burst = [g for s in all_stats for g in s["burst_gaps"]]
hesitate = [g for s in all_stats for g in s["hesitation"]]
examples = [ex for s in all_stats for ex in s["burst_examples"]]
random.seed(42)
burst_samples = examples[:]
random.shuffle(burst_samples)
profile = {
"reply_sample_count": len(reply),
"reply_p10_sec": percentile(reply, 10),
"reply_p25_sec": percentile(reply, 25),
"reply_median_sec": percentile(reply, 50),
"reply_p75_sec": percentile(reply, 75),
"reply_p90_sec": percentile(reply, 90),
"reply_p95_sec": percentile(reply, 95),
"hot_median_sec": percentile(hot, 50) or percentile(reply, 25),
"hot_p90_sec": percentile(hot, 90) or percentile(reply, 50),
"warm_median_sec": percentile(warm, 50) or percentile(reply, 50),
"cold_median_sec": percentile(cold, 50) or percentile(reply, 90),
"burst_median_sec": percentile(burst, 50) or 3.0,
"burst_p90_sec": percentile(burst, 90) or 8.0,
"pct_instant_reply": round(sum(1 for g in reply if g < 5) / len(reply), 3) if reply else 0,
"pct_slow_reply": round(sum(1 for g in reply if g >= 60) / len(reply), 3) if reply else 0,
"hesitation_rate": round(len(hesitate) / len(reply), 3) if reply else 0,
"reply_samples": subsample(reply),
"hot_reply_samples": subsample(hot, 200),
"burst_gap_samples": subsample(burst, 200),
"burst_patterns": burst_samples[:25],
"always_online_chance": 0.92,
"reply_target_min_sec": percentile(reply, 25) or 8,
"reply_target_max_sec": percentile(reply, 75) or 45,
"afk_chance": 0.08,
"keyboard_chance_cold": 0.88,
"heat_threshold": 0.55,
}
if reply:
profile["reply_mean_sec"] = round(statistics.mean(reply), 2)
if hot:
profile["hot_mean_sec"] = round(statistics.mean(hot), 2)
if burst:
profile["burst_mean_sec"] = round(statistics.mean(burst), 2)
return profile
def extract_for_persona(persona_dir: Path) -> dict:
persona = json.loads((persona_dir / "persona.json").read_text())
voice_path = persona_dir / "voice.json"
voice = json.loads(voice_path.read_text()) if voice_path.exists() else {}
persona_key = persona["screen_name"]
display = persona.get("display_name", persona_key)
aliases = tuple(voice.get("speaker_aliases") or SIDE_ALIASES.get(persona_key, ()))
account_ids = lookup_account_ids(persona_key, display)
if not account_ids:
raise SystemExit(f"no archive account for {persona_key}")
ids_sql = ",".join(str(i) for i in account_ids)
conv_ids = mysql_rows(
f"SELECT id FROM conversations WHERE "
f"(from_account IN ({ids_sql}) AND to_account={CONAN_ID}) OR "
f"(from_account={CONAN_ID} AND to_account IN ({ids_sql})) ORDER BY id"
)
all_stats = []
for cid in conv_ids:
content = mysql_content(f"SELECT content FROM conversations WHERE id={cid}")
if not content:
continue
entries = parse_entries(content, persona_key, display, aliases)
if entries:
all_stats.append(analyze_conversation(entries, persona_key))
return build_profile(all_stats)
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("target", nargs="?", help="persona dir or 'all'")
args = parser.parse_args()
if args.target == "all" or not args.target:
dirs = sorted(p for p in Path("/opt/aim-bots/personas").iterdir() if (p / "persona.json").exists())
else:
p = Path(args.target)
dirs = [p if p.is_dir() else Path("/opt/aim-bots/personas") / p]
for persona_dir in dirs:
if persona_dir.name in {"magus5311", "soewaslike"} or (persona_dir / "persona.json").exists():
try:
profile = extract_for_persona(persona_dir)
except SystemExit as exc:
print(f"skip {persona_dir.name}: {exc}")
continue
voice_path = persona_dir / "voice.json"
voice = json.loads(voice_path.read_text()) if voice_path.exists() else {}
voice["timing_profile"] = profile
voice_path.write_text(json.dumps(voice, indent=2) + "\n")
print(
f"{persona_dir.name}: n={profile['reply_sample_count']} "
f"median={profile['reply_median_sec']}s hot={profile['hot_median_sec']}s "
f"burst={profile['burst_median_sec']}s slow={profile['pct_slow_reply']}"
)
return 0
if __name__ == "__main__":
raise SystemExit(main())