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) <noreply@anthropic.com>
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user