Dungen / app.py
JohanBeytell's picture
Update app.py
f785ebe verified
Raw
History Blame Contribute Delete
8.9 kB
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 <s>)
decoded_name = decoded_name.replace("▁", " ")
decoded_name = decoded_name.replace("</s>", "")
decoded_name = decoded_name.replace("<unk>", "")
decoded_name = decoded_name.replace("<s>", "")
# 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)