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>
164 lines
5.4 KiB
Python
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()
|