import os
import random
import gradio as gr
import sentencepiece as spm
import numpy as np
import pandas as pd
import tensorflow as tf
from valx import detect_profanity, detect_hate_speech
# Configuration dictionary mapping game names to their max_seq_len
MODEL_CONFIGS = {
"Terraria": 12, "Skyrim": 13, "Witcher": 20, "WOW": 16, "Minecraft": 17,
"Dark Souls": 13, "Final Fantasy": 14, "Elden Ring": 18, "Zelda": 15,
"Dragon Age": 16, "Fallout": 13, "Darkest Dungeon": 14, "Monster Hunter": 15,
"Bloodborne": 12, "Hollow Knight": 15, "Assassin's Creed": 15, "Baldur's Gate": 14,
"Cyberpunk": 11, "Mass Effect": 13, "God Of War": 12, "Last Of Us": 5,
"Factorio": 8, "The Sims": 4, "Fortnite": 9, "League Of Legends": 12,
"Among Us": 13, "Warframe": 13, "Call of Duty": 11, "Forza Horizon": 10,
"Halo": 14, "Overwatch": 9, "Subnautica": 14, "Fantasy": 16,
# New Models
"Animal Crossing": 14, "Civilization VI": 22, "Control": 22, "Cuphead": 24,
"Dead Space": 18, "Diablo": 20, "Dota 2": 27, "EVE Online": 24, "GTA": 23,
"Hades": 20, "Metroid": 28, "Portal": 28, "Resident Evil": 21, "RimWorld": 23,
"Slay the Spire": 18, "Stardew Valley": 19, "Stellaris": 23, "Valheim": 20
}
# Global dictionary to store loaded models in memory
MODEL_CACHE = {}
def get_loaded_models(game_identifier):
"""
Lazy-loads models into memory. If the model is already in the cache,
it returns it instantly. Otherwise, it loads it from disk, caches it, and returns it.
"""
if game_identifier not in MODEL_CACHE:
file_prefix = game_identifier.lower().replace(" ", "_").replace("'", "")
# Load SentencePiece Model
sp = spm.SentencePieceProcessor()
sp.load(f"models/{file_prefix}_names.model")
# Load TFLite model
interpreter = tf.lite.Interpreter(model_path=f"models/dungen_{file_prefix}_model.tflite")
interpreter.allocate_tensors()
# Store in cache
MODEL_CACHE[game_identifier] = {
"sp": sp,
"interpreter": interpreter,
"vocab_size": sp.GetPieceSize()
}
return MODEL_CACHE[game_identifier]
def custom_pad_sequences(sequences, maxlen, padding='pre', value=0):
padded_sequences = np.full((len(sequences), maxlen), value)
for i, seq in enumerate(sequences):
if padding == 'pre':
if len(seq) <= maxlen:
padded_sequences[i, -len(seq):] = seq
else:
padded_sequences[i, :] = seq[-maxlen:]
elif padding == 'post':
if len(seq) <= maxlen:
padded_sequences[i, :len(seq)] = seq
else:
padded_sequences[i, :] = seq[:maxlen]
return padded_sequences
def generate_random_name(interpreter, vocab_size, sp, max_length=10, temperature=0.5, seed_text="", max_seq_len=12):
input_details = interpreter.get_input_details()
output_details = interpreter.get_output_details()
decoded_name = ''
if seed_text:
generated_name = seed_text
else:
random_index = np.random.randint(1, vocab_size)
random_token = sp.id_to_piece(random_index)
generated_name = random_token
for _ in range(max_length - 1):
token_list = sp.encode_as_ids(generated_name)
if len(token_list) == 0:
continue
token_list = custom_pad_sequences([token_list], maxlen=max_seq_len, padding='pre')
token_list = token_list.astype(np.float32)
interpreter.set_tensor(input_details[0]['index'], token_list)
interpreter.invoke()
predicted = interpreter.get_tensor(output_details[0]['index'])[0]
predicted = np.log(predicted + 1e-8) / temperature
predicted = np.exp(predicted) / np.sum(np.exp(predicted))
next_index = np.random.choice(range(vocab_size), p=predicted)
next_index = int(next_index)
next_token = sp.id_to_piece(next_index)
generated_name = sp.decode_pieces(sp.encode_as_pieces(generated_name) + [next_token])
decoded_name = sp.decode_pieces(sp.encode_as_pieces(generated_name))
if next_token == '' or len(decoded_name) > max_length:
break
# --- TEXT NORMALIZATION BLOCK ---
# 1. Strip out unwanted tokens (including )
decoded_name = decoded_name.replace("▁", " ")
decoded_name = decoded_name.replace("", "")
decoded_name = decoded_name.replace("", "")
decoded_name = decoded_name.replace("", "")
# 2 & 3. Normalize spacing and apply Title Case to every word
words = decoded_name.split()
normalized_name = " ".join([word.capitalize() for word in words])
# 4. Split the name and check the last part length rule
parts = normalized_name.split()
if parts and len(parts[-1]) < 3:
normalized_name = " ".join(parts[:-1])
return normalized_name.strip()
# Note: Preserving the exact parameter names (like 'type') to ensure the API contract remains unbroken
def generateNames(type, amount, max_length=30, temperature=0.5, seed_text=""):
hate_speech = detect_hate_speech(seed_text)
profanity = detect_profanity([seed_text], language='All')
if len(profanity) > 0:
gr.Warning("Profanity detected in the seed text, using an empty seed text.")
seed_text = ''
else:
if hate_speech == ['Hate Speech']:
gr.Warning('Hate speech detected in the seed text, using an empty seed text.')
seed_text = ''
elif hate_speech == ['Offensive Speech']:
gr.Warning('Offensive speech detected in the seed text, using an empty seed text.')
seed_text = ''
if type not in MODEL_CONFIGS:
return pd.DataFrame([], columns=['Names'])
# Fetch max sequence length
max_seq_len = MODEL_CONFIGS[type]
# Fetch cached models (loads them instantly if already cached)
cached_data = get_loaded_models(type)
sp = cached_data["sp"]
interpreter = cached_data["interpreter"]
vocab_size = cached_data["vocab_size"]
amount = int(amount)
max_length = int(max_length)
names = []
for _ in range(amount):
generated_name = generate_random_name(
interpreter, vocab_size, sp,
seed_text=seed_text, max_length=max_length,
temperature=temperature, max_seq_len=max_seq_len
)
stripped = generated_name.strip()
# In case the generation completely fails and returns empty
if not stripped:
names.append("Generation Failed")
continue
item_hate_speech = detect_hate_speech(stripped)
item_profanity = detect_profanity([stripped], language='All')
name = ''
if len(item_profanity) > 0:
name = "Profanity Detected"
elif item_hate_speech == ['Hate Speech']:
name = 'Hate Speech Detected'
elif item_hate_speech == ['Offensive Speech']:
name = 'Offensive Speech Detected'
else:
# Catch-all: If it's safe (or even if valx returns an empty list), keep the name!
name = stripped
names.append(name)
return pd.DataFrame(names, columns=['Names'])
demo = gr.Interface(
fn=generateNames,
inputs=[
gr.Radio(
choices=list(MODEL_CONFIGS.keys()),
label="Choose a model for your request",
value="Terraria"
),
gr.Slider(1, 100, step=1, label='Amount of Names', info='How many names to generate, must be greater than 0'),
gr.Slider(5, 60, value=30, step=1, label='Max Length', info='Max length of the generated word'),
gr.Slider(0.1, 1, value=0.5, label='Temperature', info='Controls randomness of generation, higher values = more creative, lower values = more probalistic'),
gr.Textbox('', label='Seed text (optional)', info='The starting text to begin with', max_lines=1)
],
# Removed row_count and column_count to prevent the dataframe from aggressively paginating or limiting the array visualization
outputs=[gr.Dataframe(label="Generated Names", headers=["Names"])],
title='Dungen - Name Generator',
description=(
"A fun game-inspired name generator. For an example of how to create, and train your model, like this one, head over to: https://github.com/Infinitode/OPEN-ARC/tree/main/Project-5-TWNG. There you will find our base model, the dataset we used, and implementation code in the form of a Jupyter Notebook (exported from Kaggle).\n\n"
"Try Dungen online: [Dungen AI | Advanced Neural Name Generator](https://infinitode.netlify.app/experiments/dungen-ai/)"
)
)
if __name__ == "__main__":
# Added ssr_mode=False to bypass the new Gradio 5 Async loop teardown bug
demo.launch(ssr_mode=False)