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>
210 lines
7.0 KiB
Python
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()
|