diff --git a/tooling/planet-gen/gemma_naming.py b/tooling/planet-gen/gemma_naming.py index 3baa6662e..01216eddf 100755 --- a/tooling/planet-gen/gemma_naming.py +++ b/tooling/planet-gen/gemma_naming.py @@ -1303,38 +1303,6 @@ def body_population_band(population: int) -> str: return "megaworld (1B+)" -_STEM_WORD_RE = re.compile(r"[A-Za-z][A-Za-z'\-]{2,}") - -# Tokens so generic they should never count toward stem-dominance — they -# just describe the feature type and don't carry cultural identity. -_IGNORED_STEMS = { - "the", "of", "a", "an", "and", "or", - "river", "sea", "lake", "ocean", "bay", "gulf", "range", "peak", - "peaks", "ridge", "spine", "scarp", "heights", "hollow", "run", - "beck", "water", "course", "flow", "basin", "reach", "hold", - "cross", "prime", "city", "town", "capital", "gate", "terminal", - "transit", "exchange", "concourse", "assembly", "archive", - "commons", "grounds", "square", "circle", "mountain", "mountains", - "stream", "brook", "spring", "tarn", "mere", "pool", "deep", - "expanse", "crest", "summit", "fells", "series", "sea", "maris", - "mare", "aquae", "fluvius", "terrae", -} - - -def _extract_stems(name: str) -> list[str]: - """Return the lowercase 'interesting' stems of a name — cultural - root tokens only, with 'the', 'of', 'river', 'peaks' etc. dropped. - - Used to enforce per-stem dominance caps across the full run so no - single root (e.g. 'Arcturus') can appear in hundreds of names - across 3240 bodies. - """ - return [ - t.lower() for t in _STEM_WORD_RE.findall(name) - if t.lower() not in _IGNORED_STEMS - ] - - def name_feature( voice: VoiceSubprocess, feature_type: str, @@ -1342,8 +1310,6 @@ def name_feature( blocklist: set[str], corpus: dict[tuple, set[str]], body_used: set[str], - stem_counts: dict[str, int], - stem_cap: int, system_hook: str | None, system_id: str, world_seed: int, @@ -1354,15 +1320,12 @@ def name_feature( verbose: bool, max_attempts: int = 3, ) -> str: - """Request a name from Gemma, enforce blocklist + hop/corridor dedup + - per-body cross-type dedup, fall back to the palette generator on - persistent failure. + """Request a name from Gemma, enforce blocklist + per-system dedup + + per-body cross-type dedup, skip on persistent failure. Dedup scopes: - - `corpus[(hop, corridor, feature_type)]` — cross-body dedup within - the same gate-hop distance and corridor. Systems at the same hop - in the same corridor are near neighbors and shouldn't share - feature names. Systems at different hops can. + - `corpus[(system_id, feature_type)]` — no two features of the + same type in the same system share a name. - `body_used` — per-body set across ALL feature types. Prevents the same name from appearing as a river AND an ocean AND a mountain range on the same world. @@ -1374,7 +1337,7 @@ def name_feature( palette = palette_for(corridor, system_id) planet_class = ctx.get("planet_class") or "habitable" - dedup_key = (hop, corridor, feature_type) + dedup_key = (system_id, feature_type) used = corpus.setdefault(dedup_key, set()) # Capture mode: build the attempt-0 prompt, log it, return a unique @@ -1417,24 +1380,12 @@ def name_feature( or lc in (n.lower() for n in body_used) ) - def _exceeds_stem_cap(candidate: str) -> str | None: - """Return the first stem in `candidate` that would exceed the - cap after this accept, or None if all stems are under the cap.""" - if stem_cap <= 0: - return None - for stem in _extract_stems(candidate): - if stem_counts.get(stem, 0) >= stem_cap: - return stem - return None - def _commit_name(final: str) -> None: used.add(final) body_used.add(final) - for stem in _extract_stems(final): - stem_counts[stem] = stem_counts.get(stem, 0) + 1 rejections: dict[str, int] = {"empty": 0, "placeholder": 0, "blocklist": 0, - "dedup": 0, "stem_cap": 0, "error": 0} + "dedup": 0, "error": 0} for attempt in range(max_attempts): seed = _seed_for(world_seed, body_id, local_id, attempt) @@ -1473,11 +1424,6 @@ def name_feature( rejections["dedup"] += 1 log(f" dedup '{cleaned}' {body_id}/{local_id}") continue - over_stem = _exceeds_stem_cap(cleaned) - if over_stem is not None: - rejections["stem_cap"] += 1 - log(f" stem_cap '{cleaned}' ('{over_stem}' ≥{stem_cap}) {body_id}/{local_id}") - continue _commit_name(cleaned) return cleaned @@ -1602,8 +1548,6 @@ def process_body( conn: sqlite3.Connection, blocklist: set[str], corpus: dict[tuple, set[str]], - stem_counts: dict[str, int], - stem_cap: int, system_hook: str | None, world_seed: int, hop: int, @@ -1641,22 +1585,17 @@ def process_body( # Per-body dedup set — no name may appear twice on the same body, # even across feature types. Seeded with every hand-authored name # already present so templates (Lendel, Edict, Estrade, …) keep their - # canonical identifiers and new features don't collide with them. - # Hand-authored names also count against the stem cap so those - # anchors take priority over generator output. body_used: set[str] = set() for key in ("cities", "rivers", "oceans", "mountain_ranges", "pois"): for feat in markers.get(key) or []: name = feat.get("name") if name and isinstance(name, str) and name.strip(): body_used.add(name.strip()) - for stem in _extract_stems(name): - stem_counts[stem] = stem_counts.get(stem, 0) + 1 # Cities for city in markers.get("cities") or []: if not _is_blank(city.get("name")): - corpus.setdefault((hop, corridor, _feature_type_for_city(city)), set()).add( + corpus.setdefault((system_id, _feature_type_for_city(city)), set()).add( city["name"] ) counts["preserved"] += 1 @@ -1664,7 +1603,7 @@ def process_body( feature_type = _feature_type_for_city(city) name = name_feature( voice, feature_type, ctx, blocklist, corpus, body_used, - stem_counts, stem_cap, system_hook, system_id, + system_hook, system_id, world_seed, body_id, city.get("id") or "city_?", hop, log, verbose, ) @@ -1677,12 +1616,12 @@ def process_body( # Rivers for river in markers.get("rivers") or []: if not _is_blank(river.get("name")): - corpus.setdefault((hop, corridor, "river"), set()).add(river["name"]) + corpus.setdefault((system_id, "river"), set()).add(river["name"]) counts["preserved"] += 1 continue name = name_feature( voice, "river", ctx, blocklist, corpus, body_used, - stem_counts, stem_cap, system_hook, system_id, + system_hook, system_id, world_seed, body_id, river.get("id") or "river_?", hop, log, verbose, ) @@ -1695,7 +1634,7 @@ def process_body( # Oceans / seas / lakes for water in markers.get("oceans") or []: if not _is_blank(water.get("name")): - corpus.setdefault((hop, corridor, _feature_type_for_ocean(water)), set()).add( + corpus.setdefault((system_id, _feature_type_for_ocean(water)), set()).add( water["name"] ) counts["preserved"] += 1 @@ -1703,7 +1642,7 @@ def process_body( feature_type = _feature_type_for_ocean(water) name = name_feature( voice, feature_type, ctx, blocklist, corpus, body_used, - stem_counts, stem_cap, system_hook, system_id, + system_hook, system_id, world_seed, body_id, water.get("id") or "water_?", hop, log, verbose, ) @@ -1716,14 +1655,14 @@ def process_body( # Mountain ranges for rng_feat in markers.get("mountain_ranges") or []: if not _is_blank(rng_feat.get("name")): - corpus.setdefault((hop, corridor, "mountain_range"), set()).add( + corpus.setdefault((system_id, "mountain_range"), set()).add( rng_feat["name"] ) counts["preserved"] += 1 continue name = name_feature( voice, "mountain_range", ctx, blocklist, corpus, body_used, - stem_counts, stem_cap, system_hook, system_id, + system_hook, system_id, world_seed, body_id, rng_feat.get("id") or "range_?", hop, log, verbose, ) @@ -1736,7 +1675,7 @@ def process_body( # POIs for poi in markers.get("pois") or []: if not _is_blank(poi.get("name")): - corpus.setdefault((hop, corridor, _feature_type_for_poi(poi)), set()).add( + corpus.setdefault((system_id, _feature_type_for_poi(poi)), set()).add( poi["name"] ) counts["preserved"] += 1 @@ -1744,7 +1683,7 @@ def process_body( feature_type = _feature_type_for_poi(poi) name = name_feature( voice, feature_type, ctx, blocklist, corpus, body_used, - stem_counts, stem_cap, system_hook, system_id, + system_hook, system_id, world_seed, body_id, poi.get("id") or "poi_?", hop, log, verbose, ) @@ -1862,14 +1801,6 @@ def main(): default=42, help="World seed for deterministic naming (default: 42)", ) - parser.add_argument( - "--stem-cap", - type=int, - default=20, - help="Max times any single cultural stem (e.g. 'Arcturus', " - "'Meridian') may appear across the full run before dedup " - "starts rejecting it. 0 = disabled. Default: 20.", - ) parser.add_argument( "--log", default=str(REPO_ROOT / ".tmp" / "gemma_naming.log"), @@ -1968,8 +1899,7 @@ def main(): log.raw(f" sr-voice: {MOCK_STDIO if args.mock else sr_voice_bin}") if not args.mock: log.raw(f" model: {model_path}") - log.raw(f" seed: {args.seed} refresh: every {args.refresh} requests " - f"stem-cap: {args.stem_cap}") + log.raw(f" seed: {args.seed} refresh: every {args.refresh} requests") log.raw(f" {len(markers_paths)} markers.json files to process") log.raw(f" hop {first_hop} → hop {last_hop}, core-first ordering") log.raw(f" {total_systems} distinct systems") @@ -1983,8 +1913,7 @@ def main(): ) log.raw("") - corpus: dict[tuple[str, str], set[str]] = {} - stem_counts: dict[str, int] = {} + corpus: dict[tuple, set[str]] = {} # Cache of system_id → proper_name so we can emit a header line the # first time we hit each system without re-querying per body. @@ -2046,8 +1975,6 @@ def main(): conn=conn, blocklist=blocklist, corpus=corpus, - stem_counts=stem_counts, - stem_cap=args.stem_cap, system_hook=system_gttr_hooks.get(system_id), world_seed=args.seed, hop=hop,