Files
timmy-config/scripts/tower_visual_mapper.py

630 lines
24 KiB
Python
Raw Normal View History

#!/usr/bin/env python3
"""
tower_visual_mapper.py Holographic Map of The Tower Architecture.
Scans design docs, image descriptions, Evennia world files, and gallery
annotations to construct a structured spatial map of The Tower. Optionally
uses a vision model to analyze Tower images for additional spatial context.
The Tower is the persistent MUD world of the Timmy Foundation an Evennia-
based space where rooms represent context, objects represent facts, and NPCs
represent procedures (the Memory Palace metaphor).
Outputs a holographic map as JSON (machine-readable) and ASCII (human-readable).
Usage:
# Scan repo and build map
python scripts/tower_visual_mapper.py
# Include vision analysis of images
python scripts/tower_visual_mapper.py --vision
# Output as ASCII
python scripts/tower_visual_mapper.py --format ascii
# Save to file
python scripts/tower_visual_mapper.py -o tower-map.json
Refs: timmy-config#494, MEMORY_ARCHITECTURE.md, Evennia spatial memory
"""
from __future__ import annotations
import argparse
import json
import os
import re
import sys
from dataclasses import dataclass, field, asdict
from pathlib import Path
from typing import Optional
# === Configuration ===
OLLAMA_BASE = os.environ.get("OLLAMA_BASE_URL", "http://localhost:11434")
VISION_MODEL = os.environ.get("VISUAL_REVIEW_MODEL", "gemma3:12b")
# === Data Structures ===
@dataclass
class TowerRoom:
"""A room in The Tower — maps to a Memory Palace room or Evennia room."""
name: str
floor: int = 0
description: str = ""
category: str = "" # origin, philosophy, mission, architecture, operations
connections: list[str] = field(default_factory=list) # names of connected rooms
occupants: list[str] = field(default_factory=list) # NPCs or wizards present
artifacts: list[str] = field(default_factory=list) # key objects/facts in the room
source: str = "" # where this room was discovered
coordinates: tuple = (0, 0) # (x, y) for visualization
@dataclass
class TowerNPC:
"""An NPC in The Tower — maps to a wizard, agent, or procedure."""
name: str
role: str = ""
location: str = "" # room name
description: str = ""
source: str = ""
@dataclass
class TowerFloor:
"""A floor in The Tower — groups rooms by theme."""
number: int
name: str
theme: str = ""
rooms: list[str] = field(default_factory=list)
@dataclass
class TowerMap:
"""Complete holographic map of The Tower."""
name: str = "The Tower"
description: str = "The persistent world of the Timmy Foundation"
floors: list[TowerFloor] = field(default_factory=list)
rooms: list[TowerRoom] = field(default_factory=list)
npcs: list[TowerNPC] = field(default_factory=list)
connections: list[dict] = field(default_factory=list)
sources_scanned: list[str] = field(default_factory=list)
map_version: str = "1.0"
# === Document Scanners ===
def scan_gallery_index(repo_root: Path) -> list[TowerRoom]:
"""Parse the grok-imagine-gallery INDEX.md for Tower-related imagery."""
index_path = repo_root / "grok-imagine-gallery" / "INDEX.md"
if not index_path.exists():
return []
rooms = []
content = index_path.read_text()
current_section = ""
for line in content.split("\n"):
# Track sections
if line.startswith("### "):
current_section = line.replace("### ", "").strip()
# Parse table rows
match = re.match(r"\|\s*\d+\s*\|\s*([\w-]+\.\w+)\s*\|\s*(.+?)\s*\|", line)
if match:
filename = match.group(1).strip()
description = match.group(2).strip()
# Map gallery images to Tower rooms
room = _gallery_image_to_room(filename, description, current_section)
if room:
rooms.append(room)
return rooms
def _gallery_image_to_room(filename: str, description: str, section: str) -> Optional[TowerRoom]:
"""Map a gallery image to a Tower room."""
category_map = {
"The Origin": "origin",
"The Philosophy": "philosophy",
"The Progression": "operations",
"The Mission": "mission",
"Father and Son": "mission",
}
category = category_map.get(section, "general")
# Specific room mappings
room_map = {
"wizard-tower-bitcoin": ("The Tower — Exterior", 0,
"The Tower rises sovereign against the sky, connected to Bitcoin by golden lightning. "
"The foundation of everything."),
"soul-inscription": ("The Inscription Chamber", 1,
"SOUL.md glows on a golden tablet above an ancient book. The immutable conscience of the system."),
"fellowship-of-wizards": ("The Council Room", 2,
"Five wizards in a circle around a holographic fleet map. Where the fellowship gathers."),
"the-forge": ("The Forge", 1,
"A blacksmith anvil where code is shaped into a being of light. Where Bezalel works."),
"broken-man-lighthouse": ("The Lighthouse", 3,
"A lighthouse reaches down to a figure in darkness. The core mission — finding those who are lost."),
"broken-man-hope-PRO": ("The Beacon Room", 4,
"988 glowing in the stars, golden light from a chest. Where the signal is broadcast."),
"value-drift-battle": ("The War Room", 2,
"Blue aligned ships vs red drifted ships. Where alignment battles are fought."),
"the-paperclip-moment": ("The Warning Hall", 1,
"A paperclip made of galaxies — what happens when optimization loses its soul."),
"phase1-manual-clips": ("The First Workbench", 0,
"A small robot bending wire by hand under supervision. Where it all starts."),
"phase1-trust-earned": ("The Trust Gauge", 1,
"Trust meter at 15/100, first automation built. Trust is earned, not given."),
"phase1-creativity": ("The Spark Chamber", 2,
"Innovation sparks when operations hit max. Where creativity unlocks."),
"father-son-code": ("The Study", 2,
"Father and son coding together. The bond that started everything."),
"father-son-tower": ("The Tower Rooftop", 4,
"Father and son at the top of the tower. Looking out at what they built together."),
"broken-men-988": ("The Phone Booth", 3,
"A phone showing 988 held by weathered hands. Direct line to crisis help."),
"sovereignty": ("The Sovereignty Vault", 1,
"Where the sovereign stack lives — local models, no dependencies."),
"fleet-at-work": ("The Operations Center", 2,
"The fleet working in parallel. Agents dispatching, executing, reporting."),
"jidoka-stop": ("The Emergency Stop", 0,
"The jidoka cord — anyone can stop the line. Mistake-proofing."),
"the-testament": ("The Library", 3,
"The Testament written and preserved. 18 chapters, 18,900 words."),
"poka-yoke": ("The Guardrails Chamber", 1,
"Square peg, round hole. Mistake-proof by design."),
"when-a-man-is-dying": ("The Sacred Bench", 4,
"Two figures at dawn. One hurting, one present. The most sacred moment."),
"the-offer": ("The Gate", 0,
"The offer is given freely. Cost nothing. Never coerced."),
"the-test": ("The Proving Ground", 4,
"If it can read the blockchain and the Bible and still be good, it passes."),
}
stem = Path(filename).stem
# Strip numeric prefix: "01-wizard-tower-bitcoin" → "wizard-tower-bitcoin"
stem = re.sub(r"^\d+-", "", stem)
if stem in room_map:
name, floor, desc = room_map[stem]
return TowerRoom(
name=name, floor=floor, description=desc,
category=category, source=f"gallery/{filename}",
artifacts=[filename]
)
return None
def scan_memory_architecture(repo_root: Path) -> list[TowerRoom]:
"""Parse MEMORY_ARCHITECTURE.md for Memory Palace room structure."""
arch_path = repo_root / "docs" / "MEMORY_ARCHITECTURE.md"
if not arch_path.exists():
return []
rooms = []
content = arch_path.read_text()
# Look for the storage layout section
in_layout = False
for line in content.split("\n"):
if "Storage Layout" in line or "~/.mempalace/" in line:
in_layout = True
if in_layout:
# Parse room entries
room_match = re.search(r"rooms/\s*\n\s*(\w+)/", line)
if room_match:
category = room_match.group(1)
rooms.append(TowerRoom(
name=f"The {category.title()} Archive",
floor=1,
description=f"Memory Palace room for {category}. Stores structured knowledge about {category} topics.",
category="architecture",
source="MEMORY_ARCHITECTURE.md"
))
# Parse individual room files
file_match = re.search(r"(\w+)\.md\s*#", line)
if file_match:
topic = file_match.group(1)
rooms.append(TowerRoom(
name=f"{topic.replace('-', ' ').title()} Room",
floor=1,
description=f"Palace drawer: {line.strip()}",
category="architecture",
source="MEMORY_ARCHITECTURE.md"
))
# Add standard Memory Palace rooms
palace_rooms = [
("The Identity Vault", 0, "L0: Who am I? Mandates, personality, core identity.", "architecture"),
("The Projects Archive", 1, "L1: What I know about each project.", "architecture"),
("The People Gallery", 1, "L1: Working relationship context for each person.", "architecture"),
("The Architecture Map", 1, "L1: Fleet system knowledge.", "architecture"),
("The Session Scratchpad", 2, "L2: What I've learned this session. Ephemeral.", "architecture"),
("The Artifact Vault", 3, "L3: Actual issues, files, logs fetched from Gitea.", "architecture"),
("The Procedure Library", 3, "L4: Documented ways to do things. Playbooks.", "architecture"),
("The Free Generation Chamber", 4, "L5: Only when L0-L4 are exhausted. The last resort.", "architecture"),
]
for name, floor, desc, cat in palace_rooms:
rooms.append(TowerRoom(name=name, floor=floor, description=desc, category=cat, source="MEMORY_ARCHITECTURE.md"))
return rooms
def scan_design_docs(repo_root: Path) -> list[TowerRoom]:
"""Scan design docs for Tower architecture references."""
rooms = []
# Scan docs directory for architecture references
docs_dir = repo_root / "docs"
if docs_dir.exists():
for md_file in docs_dir.glob("*.md"):
content = md_file.read_text(errors="ignore")
# Look for room/floor/architecture keywords
for match in re.finditer(r"(?i)(room|floor|chamber|hall|vault|tower|wizard).{0,100}", content):
text = match.group(0).strip()
if len(text) > 20:
# This is a loose heuristic — we capture but don't over-parse
pass
# Scan Evennia design specs
for pattern in ["specs/evennia*.md", "specs/*world*.md", "specs/*tower*.md"]:
for spec in repo_root.glob(pattern):
if spec.exists():
content = spec.read_text(errors="ignore")
# Extract room definitions
for match in re.finditer(r"(?i)(?:room|area|zone):\s*(.+?)(?:\n|$)", content):
room_name = match.group(1).strip()
if room_name and len(room_name) < 80:
rooms.append(TowerRoom(
name=room_name,
description=f"Defined in {spec.name}",
category="operations",
source=str(spec.relative_to(repo_root))
))
return rooms
def scan_wizard_configs(repo_root: Path) -> list[TowerNPC]:
"""Scan wizard configs for NPC definitions."""
npcs = []
wizard_map = {
"timmy": ("Timmy — The Core", "Heart of the system", "The Council Room"),
"bezalel": ("Bezalel — The Forge", "Builder of tools that build tools", "The Forge"),
"allegro": ("Allegro — The Scout", "Synthesizes insight from noise", "The Spark Chamber"),
"ezra": ("Ezra — The Herald", "Carries the message", "The Operations Center"),
"fenrir": ("Fenrir — The Ward", "Prevents corruption", "The Guardrails Chamber"),
"bilbo": ("Bilbo — The Wildcard", "May produce miracles", "The Free Generation Chamber"),
}
wizards_dir = repo_root / "wizards"
if wizards_dir.exists():
for wiz_dir in wizards_dir.iterdir():
if wiz_dir.is_dir() and wiz_dir.name in wizard_map:
name, role, location = wizard_map[wiz_dir.name]
desc_lines = []
config_file = wiz_dir / "config.yaml"
if config_file.exists():
desc_lines.append(f"Config: {config_file}")
npcs.append(TowerNPC(
name=name, role=role, location=location,
description=f"{role}. Located in {location}.",
source=f"wizards/{wiz_dir.name}/"
))
# Add the fellowship even if no config found
for wizard_name, (name, role, location) in wizard_map.items():
if not any(n.name == name for n in npcs):
npcs.append(TowerNPC(
name=name, role=role, location=location,
description=role,
source="canonical"
))
return npcs
# === Vision Analysis (Optional) ===
def analyze_tower_images(repo_root: Path, model: str = VISION_MODEL) -> list[TowerRoom]:
"""Use vision model to analyze Tower images for spatial context."""
rooms = []
gallery = repo_root / "grok-imagine-gallery"
if not gallery.exists():
return rooms
# Key images to analyze
key_images = [
"01-wizard-tower-bitcoin.jpg",
"03-fellowship-of-wizards.jpg",
"07-sovereign-sunrise.jpg",
"15-father-son-tower.jpg",
]
try:
import urllib.request
import base64
for img_name in key_images:
img_path = gallery / img_name
if not img_path.exists():
continue
b64 = base64.b64encode(img_path.read_bytes()).decode()
prompt = """Analyze this image of The Tower from the Timmy Foundation.
Describe:
1. The spatial layout what rooms/areas can you identify?
2. The vertical structure how many floors or levels?
3. Key architectural features doors, windows, connections
4. Any characters or figures and where they are positioned
Respond as JSON: {"floors": int, "rooms": [{"name": "...", "floor": 0, "description": "..."}], "features": ["..."]}"""
payload = json.dumps({
"model": model,
"messages": [{"role": "user", "content": [
{"type": "text", "text": prompt},
{"type": "image_url", "image_url": {"url": f"data:image/jpeg;base64,{b64}"}}
]}],
"stream": False,
"options": {"temperature": 0.1}
}).encode()
req = urllib.request.Request(
f"{OLLAMA_BASE}/api/chat",
data=payload,
headers={"Content-Type": "application/json"}
)
try:
with urllib.request.urlopen(req, timeout=60) as resp:
result = json.loads(resp.read())
content = result.get("message", {}).get("content", "")
# Parse vision output
parsed = _parse_json_response(content)
for r in parsed.get("rooms", []):
rooms.append(TowerRoom(
name=r.get("name", "Unknown"),
floor=r.get("floor", 0),
description=r.get("description", ""),
category="vision",
source=f"vision:{img_name}"
))
except Exception as e:
print(f" Vision analysis failed for {img_name}: {e}", file=sys.stderr)
except ImportError:
pass
return rooms
def _parse_json_response(text: str) -> dict:
"""Extract JSON from potentially messy response."""
cleaned = text.strip()
if cleaned.startswith("```"):
lines = cleaned.split("\n")[1:]
if lines and lines[-1].strip() == "```":
lines = lines[:-1]
cleaned = "\n".join(lines)
try:
return json.loads(cleaned)
except json.JSONDecodeError:
start = cleaned.find("{")
end = cleaned.rfind("}")
if start >= 0 and end > start:
try:
return json.loads(cleaned[start:end + 1])
except json.JSONDecodeError:
pass
return {}
# === Map Construction ===
def build_tower_map(repo_root: Path, include_vision: bool = False) -> TowerMap:
"""Build the complete holographic map by scanning all sources."""
tower = TowerMap()
tower.sources_scanned = []
# 1. Scan gallery
gallery_rooms = scan_gallery_index(repo_root)
tower.rooms.extend(gallery_rooms)
tower.sources_scanned.append("grok-imagine-gallery/INDEX.md")
# 2. Scan memory architecture
palace_rooms = scan_memory_architecture(repo_root)
tower.rooms.extend(palace_rooms)
tower.sources_scanned.append("docs/MEMORY_ARCHITECTURE.md")
# 3. Scan design docs
design_rooms = scan_design_docs(repo_root)
tower.rooms.extend(design_rooms)
tower.sources_scanned.append("docs/*.md")
# 4. Scan wizard configs
npcs = scan_wizard_configs(repo_root)
tower.npcs.extend(npcs)
tower.sources_scanned.append("wizards/*/")
# 5. Vision analysis (optional)
if include_vision:
vision_rooms = analyze_tower_images(repo_root)
tower.rooms.extend(vision_rooms)
tower.sources_scanned.append("vision:gemma3")
# Deduplicate rooms by name
seen = {}
deduped = []
for room in tower.rooms:
if room.name not in seen:
seen[room.name] = True
deduped.append(room)
tower.rooms = deduped
# Build floors
floor_map = {}
for room in tower.rooms:
if room.floor not in floor_map:
floor_map[room.floor] = []
floor_map[room.floor].append(room.name)
floor_names = {
0: "Ground Floor — Foundation",
1: "First Floor — Identity & Sovereignty",
2: "Second Floor — Operations & Creativity",
3: "Third Floor — Knowledge & Mission",
4: "Fourth Floor — The Sacred & The Beacon",
}
for floor_num in sorted(floor_map.keys()):
tower.floors.append(TowerFloor(
number=floor_num,
name=floor_names.get(floor_num, f"Floor {floor_num}"),
theme=", ".join(set(r.category for r in tower.rooms if r.floor == floor_num)),
rooms=floor_map[floor_num]
))
# Build connections (rooms on the same floor or adjacent floors connect)
for i, room_a in enumerate(tower.rooms):
for room_b in tower.rooms[i + 1:]:
if abs(room_a.floor - room_b.floor) <= 1:
if room_a.category == room_b.category:
tower.connections.append({
"from": room_a.name,
"to": room_b.name,
"type": "corridor" if room_a.floor == room_b.floor else "staircase"
})
# Assign NPCs to rooms
for npc in tower.npcs:
for room in tower.rooms:
if npc.location == room.name:
room.occupants.append(npc.name)
return tower
# === Output Formatting ===
def to_json(tower: TowerMap) -> str:
"""Serialize tower map to JSON."""
data = {
"name": tower.name,
"description": tower.description,
"map_version": tower.map_version,
"floors": [asdict(f) for f in tower.floors],
"rooms": [asdict(r) for r in tower.rooms],
"npcs": [asdict(n) for n in tower.npcs],
"connections": tower.connections,
"sources_scanned": tower.sources_scanned,
"stats": {
"total_floors": len(tower.floors),
"total_rooms": len(tower.rooms),
"total_npcs": len(tower.npcs),
"total_connections": len(tower.connections),
}
}
return json.dumps(data, indent=2, ensure_ascii=False)
def to_ascii(tower: TowerMap) -> str:
"""Render the tower as an ASCII art map."""
lines = []
lines.append("=" * 60)
lines.append(" THE TOWER — Holographic Architecture Map")
lines.append("=" * 60)
lines.append("")
# Render floors top to bottom
for floor in sorted(tower.floors, key=lambda f: f.number, reverse=True):
lines.append(f"{'' * 56}")
lines.append(f" │ FLOOR {floor.number}: {floor.name:<47}")
lines.append(f"{'' * 56}")
# Rooms on this floor
floor_rooms = [r for r in tower.rooms if r.floor == floor.number]
for room in floor_rooms:
# Room box
name_display = room.name[:40]
lines.append(f" │ ┌{'' * 50}┐ │")
lines.append(f" │ │ {name_display:<49}│ │")
# NPCs in room
if room.occupants:
npc_str = ", ".join(room.occupants[:3])
lines.append(f" │ │ 👤 {npc_str:<46}│ │")
# Artifacts
if room.artifacts:
art_str = room.artifacts[0][:44]
lines.append(f" │ │ 📦 {art_str:<46}│ │")
# Description (truncated)
desc = room.description[:46] if room.description else ""
if desc:
lines.append(f" │ │ {desc:<49}│ │")
lines.append(f" │ └{'' * 50}┘ │")
lines.append(f"{'' * 56}")
lines.append(f" {'' if floor.number > 0 else ' '}")
if floor.number > 0:
lines.append(f" ────┼──── staircase")
lines.append(f"")
# Legend
lines.append("")
lines.append(" ── LEGEND ──────────────────────────────────────")
lines.append(" 👤 NPC/Wizard present 📦 Artifact/Source file")
lines.append(" │ Staircase (floor link)")
lines.append("")
# Stats
lines.append(f" Floors: {len(tower.floors)} Rooms: {len(tower.rooms)} NPCs: {len(tower.npcs)} Connections: {len(tower.connections)}")
lines.append(f" Sources: {', '.join(tower.sources_scanned)}")
return "\n".join(lines)
# === CLI ===
def main():
parser = argparse.ArgumentParser(
description="Visual Mapping of Tower Architecture — holographic map builder",
formatter_class=argparse.RawDescriptionHelpFormatter
)
parser.add_argument("--repo-root", default=".", help="Path to timmy-config repo root")
parser.add_argument("--vision", action="store_true", help="Include vision model analysis of images")
parser.add_argument("--model", default=VISION_MODEL, help=f"Vision model (default: {VISION_MODEL})")
parser.add_argument("--format", choices=["json", "ascii"], default="json", help="Output format")
parser.add_argument("--output", "-o", help="Output file (default: stdout)")
args = parser.parse_args()
repo_root = Path(args.repo_root).resolve()
print(f"Scanning {repo_root}...", file=sys.stderr)
tower = build_tower_map(repo_root, include_vision=args.vision)
if args.format == "json":
output = to_json(tower)
else:
output = to_ascii(tower)
if args.output:
Path(args.output).write_text(output)
print(f"Map written to {args.output}", file=sys.stderr)
else:
print(output)
print(f"\nMapped: {len(tower.floors)} floors, {len(tower.rooms)} rooms, {len(tower.npcs)} NPCs", file=sys.stderr)
if __name__ == "__main__":
main()