Files
settled-reach/tooling/planet-gen/test_batch_naming.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

210 lines
7.0 KiB
Python

#!/usr/bin/env python3
"""Test batch naming: exercises naming_core against real Gemma 4.
Usage:
python3 tooling/planet-gen/test_batch_naming.py
"""
import json
import os
import signal
import subprocess
import sys
import hashlib
import time
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent))
from gemma_naming import (
CORRIDOR_SUBSTYLES,
DEFAULT_SUBSTYLES,
_PROMPT_CONFIG,
)
from naming_core import (
build_batch_prompt,
parse_batch_response,
select_distinct,
)
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
# Full system simulations — 5 bodies each, accumulating taken list
SYSTEM_SIMS = [
{
"name": "Ran", "corridor": "core",
"inflection": "English countryside, rural, agricultural settlers",
"hook": "RAN does not import food.",
"bodies": [
("GJ144b", "ambition"), ("GJ144c", "community"),
("GJ144d", "fear"), ("GJ144e", "hope"), ("GJ144e-1", "loss"),
],
},
{
"name": "Groombridge", "corridor": "core",
"inflection": "British colonial settlement era",
"hook": "GROOMBRIDGE is where the money lives.",
"bodies": [
("GJ380b", "wealth"), ("GJ380c", "pride"),
("GJ380d", "industry"), ("GJ380e", "ambition"), ("GJ380f", "fleeting"),
],
},
{
"name": "Cairnside", "corridor": "deep_frontier",
"inflection": "frontier descriptive, geographic features named by surveyors",
"hook": "CAIRNSIDE is a materials science program running for forty years.",
"bodies": [
("GJ892b", "defiance"), ("GJ892c", "isolation"),
("GJ892d", "fear"), ("GJ892e", "hope"), ("GJ892f", "loss"),
],
},
{
"name": "Ratnagiri", "corridor": "north_reach",
"inflection": "South African English settler",
"hook": "RATNAGIRI has a monopoly on its primary export that no one engineered.",
"bodies": [
("GJ575Ab", "pride"), ("GJ575Ac", "community"),
("GJ575Ad", "industry"), ("GJ575Ae", "devotion"), ("GJ575Af", "ambition"),
],
},
]
# Build test list
TESTS = []
for sim in SYSTEM_SIMS:
for body_id, mood in sim["bodies"]:
TESTS.append({
"label": f"{sim['name']}{body_id} (mood: {mood})",
"system": sim["name"], "body": body_id, "corridor": sim["corridor"],
"inflection": sim["inflection"],
"feature_type": "mountain_range", "count": 8,
"taken": f"__accumulate_{sim['name']}__",
"hook": sim["hook"], "mood": mood,
})
def main():
cmd = ["distrobox", "enter", DISTROBOX, "--",
str(BIN),
"--model", str(MODEL),
"--ctx-size", str(CTX_SIZE)]
print("starting sr-voice-tooling...", flush=True)
proc = subprocess.Popen(
cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE,
stderr=subprocess.DEVNULL, text=True, bufsize=1,
start_new_session=True,
)
accum: dict[str, list[str]] = {}
for test in TESTS:
taken = test["taken"]
if isinstance(taken, str) and taken.startswith("__accumulate_"):
key = taken
taken = list(accum.get(key, []))
# Primary batch
prompt = build_batch_prompt(
feature_type=test["feature_type"],
inflection=test["inflection"],
count=test["count"],
taken=taken,
prompt_config=_PROMPT_CONFIG,
system_name=test["system"],
body_name=test["body"],
system_hook=test["hook"],
mood=test.get("mood"),
ctx_size=CTX_SIZE,
)
seed = int(hashlib.sha256(
f"batch|{test['body']}|{test['feature_type']}".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}"
candidates = parse_batch_response(raw)
selected = select_distinct(candidates, test["count"], taken)
# Refill from adjacent register if short
if len(selected) < test["count"]:
shortfall = test["count"] - len(selected)
refill_taken = taken + selected
corridor = test.get("corridor", "core")
substyles = CORRIDOR_SUBSTYLES.get(corridor, DEFAULT_SUBSTYLES)
primary_idx = next(
(i for i, s in enumerate(substyles)
if s["inflection"] == test["inflection"]),
0,
)
refill_idx = (primary_idx + 1) % len(substyles)
refill_inflection = substyles[refill_idx]["inflection"]
refill_prompt = build_batch_prompt(
feature_type=test["feature_type"],
inflection=refill_inflection,
count=shortfall * 3,
taken=refill_taken,
prompt_config=_PROMPT_CONFIG,
system_name=test["system"],
body_name=test["body"],
system_hook=test["hook"],
mood=test.get("mood"),
ctx_size=CTX_SIZE,
)
refill_seed = int(hashlib.sha256(
f"refill|{test['body']}|{test['feature_type']}".encode()
).hexdigest()[:8], 16)
proc.stdin.write(json.dumps({"prompt": refill_prompt, "seed": refill_seed}) + "\n")
proc.stdin.flush()
t1 = time.time()
refill_line = proc.stdout.readline()
refill_elapsed = time.time() - t1
try:
refill_resp = json.loads(refill_line)
refill_raw = refill_resp.get("text", "")
except (json.JSONDecodeError, TypeError):
refill_raw = ""
refill_candidates = parse_batch_response(refill_raw)
extra = select_distinct(refill_candidates, shortfall, refill_taken)
print(f" REFILL ({refill_inflection}): {len(refill_candidates)} cand → {len(extra)} new: {extra}")
selected.extend(extra)
print(f" {test['label']}")
print(f" {len(candidates)} cand → {len(selected)} selected ({elapsed:.1f}s) taken={len(taken)}")
print(f" {selected}")
if isinstance(test["taken"], str) and test["taken"].startswith("__accumulate_"):
key = test["taken"]
accum.setdefault(key, []).extend(selected)
print(f" [{test['system']}: {len(accum[key])} total]")
proc.stdin.close()
try:
os.killpg(os.getpgid(proc.pid), signal.SIGTERM)
except Exception:
pass
proc.wait()
print()
if __name__ == "__main__":
main()