diff --git a/tooling/planet-gen/gemma_naming.py b/tooling/planet-gen/gemma_naming.py index 4cbae2ab7..3baa6662e 100755 --- a/tooling/planet-gen/gemma_naming.py +++ b/tooling/planet-gen/gemma_naming.py @@ -176,71 +176,95 @@ class Logger: # `star_systems.geographic_sector` directly — that column is the real # source of corridor identity in systems.db (cultural_corridor is a # legacy field that was never populated beyond sol-gateway-axis). -CORRIDOR_PALETTES: dict[str, dict[str, str]] = { +CORRIDOR_SUBSTYLES: dict[str, list[dict[str, str]]] = { + # Each corridor has a list of sub-style inflections. The pipeline + # picks one per body via hash(body_id) so neighbouring bodies on the + # same planet get different registers, and Gemma's narrow per-register + # vocabulary (~15 stems) stays fresh across hundreds of bodies. + # # IMPORTANT: the inflection is a DOMINANT bias, not a hard lock. - # Earlier iterations used single-culture labels like - # "administrative English" or "Korean/Japanese/Taiwanese" which - # Gemma 2 2B interpreted as "produce ONLY in this register" — and - # every core body came out anglophone, every east_reach body came - # out East Asian, etc. The real Earth diaspora in the setting is - # cosmopolitan: a British surveyor on an east_reach moon still - # names a river after their aunt in Dorset. The inflections now - # explicitly name the dominant register AND invite diaspora - # variety so Gemma samples from the full cross-cultural few-shot - # pool instead of collapsing to the dominant label. - "core": { - "inflection": "Gateway-era Earth diaspora, mostly English, " - "all Earth cultures welcome", - "examples": "Meridian, Concord, East Ridge, Landing, Tanaka, " - "Rahman, Okafor, Ribeiro", - }, - "north_reach": { - "inflection": "British/Australian/Irish dominant, all Earth " - "cultures welcome", - "examples": "Wolcott, Mildern, Ashbourne, Kiln, Briarfell, " - "Tarndale, Ribeiro, Nakamura, Kowalski", - }, - "south_reach": { - "inflection": "Portuguese/Swahili/Cape Verdean/Brazilian " - "dominant, all Earth cultures welcome", - "examples": "Vargas, Inhaca, Monteforte, Serra, Ribeiro, " - "Kilimi, Holmberg, Fairview, Tanaka", - }, - "east_reach": { - "inflection": "Korean/Japanese/Taiwanese dominant, all Earth " - "cultures welcome", - "examples": "Hanyang, Takamine, Seoraksan, Tsukuri, Ginoza, " - "Baektu, Kellogg, Oliveira, Novak", - }, - "west_reach": { - "inflection": "German/Dutch/Nordic dominant, all Earth cultures " - "welcome", - "examples": "Vanebach, Kloosterdam, Bergfjord, Hellekade, " - "Straend, Cooper, Vargas, Nakamura", - }, - "deep_frontier": { - "inflection": "frontier founder-name era, any Earth culture", - "examples": "Okafor Reach, Stenner Cross, Weller Hold, Pruitt " - "Basin, Vickery Hold, Kellogg Run, Nakamura Post, " - "Ribeiro Landing", - }, - # Legacy keys retained for backward compatibility with the - # cultural_corridor column on the one system that uses it. - "sol-gateway-axis": { - "inflection": "Gateway-era Earth diaspora, mostly English, " - "all Earth cultures welcome", - "examples": "Meridian, Concord, Cardinal, Prefecture, Tanaka, " - "Rahman, Okafor", - }, - "inner_corridor": { - "inflection": "Gateway-era Earth diaspora, mostly English, " - "all Earth cultures welcome", - "examples": "Meridian, Concord, East Ridge, Landing, Tanaka, " - "Rahman, Okafor", - }, + # A British surveyor on an east_reach moon still names a river after + # their aunt in Dorset. Each sub-style explicitly names its register + # AND invites diaspora variety. + "core": [ + {"inflection": "English countryside, rural, agricultural settlers", + "examples": "Thornbury, Bramblewood, Millbrook, Ashford, Weston"}, + {"inflection": "British colonial settlement era", + "examples": "New Bristol, Port Augusta, Kingstown, Admiralty, Georgetown"}, + {"inflection": "American frontier, practical, geographic", + "examples": "Dusty Creek, Twin Oaks, Cedar Flat, Hawk's Hollow, Red Bluff"}, + {"inflection": "American municipal, administrative, cosmopolitan", + "examples": "Prospect Heights, Liberty, Union, Meridian, Commonwealth"}, + {"inflection": "Classical references, institutional, civic", + "examples": "Concordia, Aurelius, Prefecture, Senate Landing, Forum"}, + {"inflection": "Australian and New Zealand settler", + "examples": "Redfern, Glenelg, Wollongong, Kaikoura, Hawke's Bay"}, + ], + "north_reach": [ + {"inflection": "English rural, village and parish names", + "examples": "Wolcott, Mildern, Ashbourne, Briarfell, Tarndale"}, + {"inflection": "Scottish Highland and Lowland place-names", + "examples": "Glenmoray, Dunfermline, Kinross, Brae, Dalwhinnie"}, + {"inflection": "Australian outback, station and property names", + "examples": "Redfern, Birdsville, Tennant, Woomera, Coober"}, + {"inflection": "Irish rural and coastal settlement", + "examples": "Ballymore, Kilrush, Dunmore, Tralee, Skellig"}, + {"inflection": "South African English settler", + "examples": "Grahamstown, Oudtshoorn, Stellenbosch, Graaff, Beaufort"}, + ], + "south_reach": [ + {"inflection": "Portuguese colonial era, Iberian", + "examples": "Monteforte, Serra, Tavira, Oliveira, Porto Novo"}, + {"inflection": "Brazilian interior, frontier settlement", + "examples": "Ribeirão, Campo Largo, Várzea, Ilhabela, Pinheiro"}, + {"inflection": "East African Swahili coastal", + "examples": "Inhambane, Kilimi, Ngola, Manhica, Quelimane"}, + {"inflection": "Cape Verdean and West African", + "examples": "Cabo, Moçambo, Ribeira, Mindelo, Tarrafal"}, + {"inflection": "Angolan and Mozambican settlement", + "examples": "Huambo, Lobito, Nampula, Lichinga, Benguela"}, + ], + "east_reach": [ + {"inflection": "Korean place-name tradition", + "examples": "Hanyang, Seorak, Baektu, Saeyeon, Taegong"}, + {"inflection": "Japanese rural and coastal settlement", + "examples": "Takamine, Ginoza, Tsukuri, Aomori, Fukagawa"}, + {"inflection": "Taiwanese and Hakka settler", + "examples": "Jiufen, Beigang, Hsinchu, Meinong, Tainan"}, + {"inflection": "Filipino settler community", + "examples": "Batangas, Legazpi, Tuguegarao, Zambales, Tarlac"}, + {"inflection": "Mixed East Asian diaspora, cosmopolitan", + "examples": "Naruhan, Morimine, Kōzan, Midori, Kawasaki"}, + ], + "west_reach": [ + {"inflection": "German settlement, orderly and compound names", + "examples": "Altdorf, Drachenberg, Feldberg, Krakenberg, Lüneborg"}, + {"inflection": "Dutch colonial, low-country", + "examples": "Kloosterdam, Hoogland, Oudewater, Nieuwpoort, Voorhout"}, + {"inflection": "Nordic and Scandinavian", + "examples": "Sørholm, Torsfell, Bergfjord, Lindeborg, Nordhölm"}, + {"inflection": "Polish and Czech settler", + "examples": "Krakowice, Bystrica, Wieliczka, Tarnów, Ostrava"}, + {"inflection": "Baltic and Finnish settler", + "examples": "Järvenpää, Tallinna, Pärnu, Turku, Rakvere"}, + ], + "deep_frontier": [ + {"inflection": "frontier founder-name era, surname-first, any Earth culture", + "examples": "Okafor Reach, Stenner Cross, Weller Hold, Pruitt Basin"}, + {"inflection": "frontier descriptive, geographic features named by surveyors", + "examples": "Red Mesa, Dry Fork, Iron Flat, Long Ridge, Dust Basin"}, + {"inflection": "frontier outpost, functional and military", + "examples": "Forward Post, Relay Station, Survey Camp, Waypoint, Anchor"}, + ], } -DEFAULT_PALETTE = CORRIDOR_PALETTES["core"] +# Legacy aliases +CORRIDOR_SUBSTYLES["sol-gateway-axis"] = CORRIDOR_SUBSTYLES["core"] +CORRIDOR_SUBSTYLES["inner_corridor"] = CORRIDOR_SUBSTYLES["core"] +CORRIDOR_SUBSTYLES["inner_orbit"] = CORRIDOR_SUBSTYLES["core"] +CORRIDOR_SUBSTYLES["frontier"] = CORRIDOR_SUBSTYLES["deep_frontier"] + +DEFAULT_SUBSTYLES = CORRIDOR_SUBSTYLES["core"] # Processing order for the main run — core first so those bodies win # the dedup race and the outer sectors fall into the palette fallback @@ -255,10 +279,16 @@ SECTOR_PRIORITY: dict[str, int] = { } -def palette_for(corridor: str | None) -> dict[str, str]: - if not corridor: - return DEFAULT_PALETTE - return CORRIDOR_PALETTES.get(corridor, DEFAULT_PALETTE) +def palette_for(corridor: str | None, system_id: str = "") -> dict[str, str]: + """Pick a sub-style for this system within its corridor. + + All bodies in the same system get the same sub-style (consistent + cultural register per star system). Different systems rotate through + the sub-style list via hash(system_id). + """ + substyles = CORRIDOR_SUBSTYLES.get(corridor or "core", DEFAULT_SUBSTYLES) + idx = int(hashlib.sha256(system_id.encode()).hexdigest()[:8], 16) % len(substyles) + return substyles[idx] # --------------------------------------------------------------------------- @@ -1315,6 +1345,7 @@ def name_feature( stem_counts: dict[str, int], stem_cap: int, system_hook: str | None, + system_id: str, world_seed: int, body_id: str, local_id: str, @@ -1340,7 +1371,7 @@ def name_feature( if feature_type not in _PROMPT_CONFIG: return fallback_name(corridor, feature_type, _seed_for(world_seed, body_id, local_id, 0)) - palette = palette_for(corridor) + palette = palette_for(corridor, system_id) planet_class = ctx.get("planet_class") or "habitable" dedup_key = (hop, corridor, feature_type) @@ -1633,7 +1664,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, + stem_counts, stem_cap, system_hook, system_id, world_seed, body_id, city.get("id") or "city_?", hop, log, verbose, ) @@ -1651,7 +1682,7 @@ def process_body( continue name = name_feature( voice, "river", ctx, blocklist, corpus, body_used, - stem_counts, stem_cap, system_hook, + stem_counts, stem_cap, system_hook, system_id, world_seed, body_id, river.get("id") or "river_?", hop, log, verbose, ) @@ -1672,7 +1703,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, + stem_counts, stem_cap, system_hook, system_id, world_seed, body_id, water.get("id") or "water_?", hop, log, verbose, ) @@ -1692,7 +1723,7 @@ def process_body( continue name = name_feature( voice, "mountain_range", ctx, blocklist, corpus, body_used, - stem_counts, stem_cap, system_hook, + stem_counts, stem_cap, system_hook, system_id, world_seed, body_id, rng_feat.get("id") or "range_?", hop, log, verbose, ) @@ -1713,7 +1744,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, + stem_counts, stem_cap, system_hook, system_id, world_seed, body_id, poi.get("id") or "poi_?", hop, log, verbose, )