Files
settled-reach/tooling/planet-gen/test_register_selection.py
T
jpmschweitzerandClaude Opus 4.6 9ad9b88d7c feat(tooling): Gemma 4 batch naming pipeline with wiki-grounded register selection (#833)
Replace the one-at-a-time Gemma 2 naming pipeline with a batch-oriented
Gemma 4 E2B pipeline. Key changes:

- naming_core.py: shared library with Levenshtein distinctiveness ranking,
  batch prompt building, mood injection pool, name validation, and
  adjacent-register refill logic
- Wiki-grounded register selection: per-system LLM call picks the cultural
  register based on wiki/GTTR content instead of hash randomizer
- Batch naming: requests N*2 names per call, ranks by word-average
  Levenshtein distance, fills quota from most-distinct candidates
- Mood pool: 13 emotional seeds randomized per-body for vocabulary
  divergence (ambition, fear, isolation, defiance, etc.)
- Adjacent-register refill: when primary register exhausts, automatically
  switches to next corridor substyle
- Inhabited-first body ordering: habitable worlds get first pick of
  register vocabulary, barren moons get leftovers
- Process group cleanup: SIGTERM/SIGKILL the full distrobox chain on
  subprocess refresh to prevent GPU zombie processes
- qa_naming.py: QA report, fix_fewshot_bleed.py: post-hoc fix script
- test_batch_naming.py, test_register_selection.py: test harnesses

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-17 16:09:23 +02:00

164 lines
5.4 KiB
Python

#!/usr/bin/env python3
"""Quick test: send register-selection prompts to sr-voice for a few
systems and print what Gemma actually picks.
Usage:
python3 tooling/planet-gen/test_register_selection.py
"""
import json
import subprocess
import sys
import hashlib
import re
import time
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent))
from gemma_naming import (
load_wiki_context,
CORRIDOR_SUBSTYLES,
DEFAULT_SUBSTYLES,
palette_for,
_extract_cultural_lines,
)
BIN = Path.home() / "Projects/settled-reach/binaries/sr-voice-tooling"
MODEL = Path.home() / "Projects/settled-reach/models/gemma-4.gguf"
DISTROBOX = "reach-build"
CTX_SIZE = 1024
TEST_SYSTEMS = [
("GJ 411", "south_reach", "Lalande — Iberian/Portuguese"),
("GJ 884", "south_reach", "Matamba — Angolan/Lusophone"),
("GJ 506", "west_reach", "Dokkum — Dutch/Frisian"),
("GJ 581", "west_reach", "Rødvik — Nordic"),
("GJ 34B", "east_reach", "Yongjin — Korean"),
("GJ 205", "east_reach", "Kurashiki — Japanese"),
("GJ 71", "core", "Gateway — administrative hub"),
("GJ 144", "core", "Ran — agricultural"),
]
def build_prompt(system_id: str, corridor: str) -> tuple[str, list[dict]]:
"""Build the register selection prompt. Returns (prompt, substyles)."""
substyles = CORRIDOR_SUBSTYLES.get(corridor, DEFAULT_SUBSTYLES)
wiki_text, gttr_text = load_wiki_context(system_id)
options = []
for idx, style in enumerate(substyles, 1):
options.append(f"{idx}. {style['inflection']}")
option_block = "\n".join(options)
context_parts = []
if gttr_text:
for para in gttr_text.strip().split("\n\n"):
cleaned = para.replace("#", "").strip()
if cleaned.startswith("THE DRIFTER") or cleaned.startswith("DRIFTER"):
continue
if not cleaned or len(cleaned) < 20:
continue
context_parts.append(cleaned[:300])
break
if wiki_text:
cultural = _extract_cultural_lines(wiki_text)
if cultural:
context_parts.append(cultural)
context = "\n".join(context_parts)
preamble = (
"Match the star system to the best cultural naming register.\n\n"
"System: Neustadt — German-heritage industrial town, west corridor, orderly municipal governance.\n"
"1. German settlement 2. Dutch colonial 3. Nordic 4. Polish/Czech 5. Baltic/Finnish\n"
"Best: 1\n\n"
"System: Matsue — Japanese precision manufacturing hub, east corridor.\n"
"1. Korean 2. Japanese 3. Taiwanese/Hakka 4. Filipino 5. Mixed East Asian\n"
"Best: 2\n\n"
"System: "
)
tail = f"\n{option_block}\nBest (number only):"
max_prompt_chars = (CTX_SIZE - 16) * 4
budget = max_prompt_chars - len(preamble) - len(tail)
if budget < 100:
budget = 100
if len(context) > budget:
context = context[:budget]
prompt = f"{preamble}{context}{tail}"
return prompt, substyles
def main():
# Show prompt sizes first
print("Prompt token estimates (rough: chars/4):")
for system_id, corridor, note in TEST_SYSTEMS:
prompt, _ = build_prompt(system_id, corridor)
est_tokens = len(prompt) // 4
print(f" {system_id:<8s} ~{est_tokens:>4d} tokens ({len(prompt)} chars) {note}")
print()
cmd = ["distrobox", "enter", DISTROBOX, "--",
str(BIN),
"--model", str(MODEL),
"--ctx-size", str(CTX_SIZE)]
print(f"starting sr-voice (ctx_size={CTX_SIZE})...", flush=True)
proc = subprocess.Popen(
cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE,
stderr=subprocess.DEVNULL, text=True, bufsize=1,
)
results = []
for system_id, corridor, note in TEST_SYSTEMS:
prompt, substyles = build_prompt(system_id, corridor)
seed = int(hashlib.sha256(
f"register|{system_id}|0".encode()
).hexdigest()[:8], 16)
req = json.dumps({"prompt": prompt, "seed": seed})
proc.stdin.write(req + "\n")
proc.stdin.flush()
t0 = time.time()
resp_line = proc.stdout.readline()
elapsed = time.time() - t0
try:
resp = json.loads(resp_line)
raw = resp.get("text", resp.get("error", ""))
except (json.JSONDecodeError, TypeError):
raw = f"ERR:{resp_line!r}"
# Parse
digits = re.search(r"\d+", raw.strip() or "")
if digits:
try:
choice = int(digits.group())
if 1 <= choice <= len(substyles):
picked = f"#{choice} {substyles[choice - 1]['inflection']}"
else:
picked = f"OUT OF RANGE ({choice})"
except ValueError:
picked = "PARSE FAIL"
else:
picked = f"FAIL: {raw[:60]}"
hash_pal = palette_for(corridor, system_id)
results.append((system_id, note, raw.strip()[:12], picked,
hash_pal["inflection"], elapsed))
proc.stdin.close()
proc.wait()
print()
print(f"{'System':<8s} {'Raw':<12s} {'Gemma picked':<48s} {'Hash fallback':<45s} {'Time':>5s}")
print("-" * 130)
for system_id, note, raw, picked, hash_pick, elapsed in results:
print(f"{system_id:<8s} {raw:<12s} {picked:<48s} {hash_pick:<45s} {elapsed:4.1f}s")
print(f" └ {note}")
print()
if __name__ == "__main__":
main()