From d57f0566d2d230ee7e813d8a0c349b2937b13449 Mon Sep 17 00:00:00 2001 From: Jeroen Schweitzer Date: Sat, 2 May 2026 18:40:21 +0200 Subject: [PATCH] fix(atlas): suffix monotony auto-fix + cultural-history prompting (#886) gemma_naming.py now re-queries affected bodies when >40% suffix clustering is detected. naming_core.py build_batch_prompt accepts cultural_history param threading secondary corridor substyles into the few-shot prompt for richer cross-cultural name blending. Co-Authored-By: Claude Opus 4.6 (1M context) --- tooling/planet-gen/gemma_naming.py | 83 +++++++++++++++++++++++++++--- tooling/planet-gen/naming_core.py | 19 ++++++- 2 files changed, 92 insertions(+), 10 deletions(-) diff --git a/tooling/planet-gen/gemma_naming.py b/tooling/planet-gen/gemma_naming.py index d7b8d35cc..96a788788 100755 --- a/tooling/planet-gen/gemma_naming.py +++ b/tooling/planet-gen/gemma_naming.py @@ -1842,6 +1842,19 @@ def process_body( substyles = CORRIDOR_SUBSTYLES.get(corridor, DEFAULT_SUBSTYLES) mood = mood_for_body(body_id, world_seed) + # Build cultural-history context for the prompt (#886 §6): + # Collect the inflection descriptions of all *secondary* registers in + # this corridor so the model sees the full settlement layering — e.g. + # "Scottish Highland" as primary, but also the Irish and Australian + # substyles that represent earlier or interleaved waves of settlers. + # Limited to 3 secondary styles to keep the prompt concise. + _secondary_inflections = [ + s["inflection"] for s in substyles if s["inflection"] != inflection + ][:3] + cultural_history: str | None = ( + "; ".join(_secondary_inflections) if _secondary_inflections else None + ) + # Helper: batch-name blank features in a marker section def _batch_fill( section_key: str, @@ -1895,6 +1908,7 @@ def process_body( mood=mood, body_id=body_id, world_seed=world_seed, + cultural_history=cultural_history, ctx_size=voice.ctx_size, ) @@ -1926,11 +1940,11 @@ def process_body( _batch_fill("mountain_ranges", lambda f: "mountain_range", "mountain_ranges") _batch_fill("pois", _feature_type_for_poi, "pois") - # Mountain suffix monotony check (#853 §3): + # Mountain suffix monotony check + auto-fix (#853 §3, #886 §3): # If >40% of mountain names on a single body share a trailing word, - # flag it. We don't re-query in the batch pipeline (no voice access here) - # but record a warning so the batch runner can surface bodies that need - # a targeted re-run. + # re-query with the offending names added to `taken` so the model is + # forced to diversify. One retry per body; if the retry still clusters + # (rare), record a warning for post-run inspection. mountain_names = [ f.get("name", "") for f in (markers.get("mountain_ranges") or []) if f.get("name") @@ -1944,11 +1958,64 @@ def process_body( dominant = max(suffix_counts, key=lambda k: suffix_counts[k]) dominant_frac = suffix_counts[dominant] / len(mountain_names) if dominant_frac > 0.40: - counts["suffix_monotony_warning"] = ( - f"mountain suffix '{dominant}' on " - f"{suffix_counts[dominant]}/{len(mountain_names)} " - f"features ({dominant_frac:.0%}) — re-run targeting this body" + # Targeted retry: identify features with the dominant suffix, + # re-request names for them with the monotonous names as `taken`. + offending_features = [ + f for f in (markers.get("mountain_ranges") or []) + if f.get("name") and f["name"].split()[-1].lower() == dominant + ] + retry_taken = ( + list(body_used) + + list(corpus.get((corridor, "mountain_range"), set())) ) + retry_names = name_features_batch( + voice=voice, + feature_type="mountain_range", + count=len(offending_features), + inflection=inflection, + corridor=corridor, + corridor_substyles=substyles, + taken=retry_taken, + prompt_config=_PROMPT_CONFIG, + system_name=ctx.get("system_proper_name"), + body_name=ctx.get("body_proper_name"), + system_hook=system_hook, + mood=mood, + body_id=body_id, + world_seed=world_seed + 1, # bump seed to force different output + cultural_history=cultural_history, + ctx_size=voice.ctx_size, + ) + for i, feat in enumerate(offending_features): + if i < len(retry_names): + old_name = feat["name"] + feat["name"] = retry_names[i] + body_used.discard(old_name) + body_used.add(retry_names[i]) + corpus.setdefault((corridor, "mountain_range"), set()).discard(old_name) + corpus.setdefault((corridor, "mountain_range"), set()).add(retry_names[i]) + changed = True + # Re-check after retry; record warning if still clustered + mountain_names_after = [ + f.get("name", "") for f in (markers.get("mountain_ranges") or []) + if f.get("name") + ] + suffix_counts_after: dict[str, int] = {} + for mn in mountain_names_after: + words = mn.split() + if words: + suffix_counts_after[words[-1].lower()] = ( + suffix_counts_after.get(words[-1].lower(), 0) + 1 + ) + if suffix_counts_after: + dominant_after = max(suffix_counts_after, key=lambda k: suffix_counts_after[k]) + dominant_frac_after = suffix_counts_after[dominant_after] / len(mountain_names_after) + if dominant_frac_after > 0.40: + counts["suffix_monotony_warning"] = ( + f"mountain suffix '{dominant_after}' still on " + f"{suffix_counts_after[dominant_after]}/{len(mountain_names_after)} " + f"features ({dominant_frac_after:.0%}) after retry" + ) # Infrastructure naming (#853 §7): # Assign deterministic city-pair names to unnamed roads and railroads. diff --git a/tooling/planet-gen/naming_core.py b/tooling/planet-gen/naming_core.py index f7837f587..92fd9088b 100644 --- a/tooling/planet-gen/naming_core.py +++ b/tooling/planet-gen/naming_core.py @@ -230,6 +230,7 @@ def build_batch_prompt( body_name: str | None = None, system_hook: str | None = None, mood: str | None = None, + cultural_history: str | None = None, ctx_size: int = 1024, ) -> str: """Build a batch naming prompt asking for N names in one call. @@ -238,6 +239,10 @@ def build_batch_prompt( with few-shot examples showing comma-separated lists. The model pattern-completes the list. + `cultural_history` threads secondary cultural registers into the + prompt so names reflect the layered settlement history of a corridor + rather than only the primary inflection style (#886 §6). + The prompt is truncated to fit within ctx_size tokens (rough estimate: 1 token ≈ 4 chars). """ @@ -274,9 +279,11 @@ def build_batch_prompt( lines.append(". ".join(ident) + ".") if system_hook: lines.append(f"About the system: {system_hook}") + if cultural_history: + lines.append(f"Settlement history: {cultural_history}") if taken: lines.append(f"Already used (do NOT repeat): {', '.join(taken)}") - if system_name or body_name or system_hook or taken: + if system_name or body_name or system_hook or cultural_history or taken: lines.append("") # Few-shot examples showing batch format @@ -292,7 +299,7 @@ def build_batch_prompt( max_chars = (ctx_size - 16) * 4 # 16 tokens headroom for output budget = max_chars - len(fixed) - len(tail) - 2 # 2 for newlines if budget < 0: - # Trim the taken list to fit + # Trim taken list first (preserving cultural_history context) while taken and budget < 0: taken = taken[:-1] lines_rebuild = [preamble, ""] @@ -305,6 +312,8 @@ def build_batch_prompt( lines_rebuild.append(". ".join(ident) + ".") if system_hook: lines_rebuild.append(f"About the system: {system_hook}") + if cultural_history: + lines_rebuild.append(f"Settlement history: {cultural_history}") if taken: lines_rebuild.append(f"Already used (do NOT repeat): {', '.join(taken)}") lines_rebuild.append("") @@ -336,6 +345,7 @@ def name_features_batch( mood: str | None, body_id: str, world_seed: int, + cultural_history: str | None = None, ctx_size: int = 1024, ) -> list[str]: """Generate `count` names for a feature type using batch prompting. @@ -346,6 +356,9 @@ def name_features_batch( 3. Rank by distinctiveness via Levenshtein, pick top `count`. 4. If short, refill from the next adjacent register in the corridor. 5. Return the final list of names. + + `cultural_history` is forwarded to build_batch_prompt to enrich the + prompt with secondary cultural context (#886 §6). """ prompt = build_batch_prompt( feature_type=feature_type, @@ -357,6 +370,7 @@ def name_features_batch( body_name=body_name, system_hook=system_hook, mood=mood, + cultural_history=cultural_history, ctx_size=ctx_size, ) @@ -396,6 +410,7 @@ def name_features_batch( body_name=body_name, system_hook=system_hook, mood=mood, + cultural_history=cultural_history, ctx_size=ctx_size, )