AI-RPG / app.py
necdet.sayiner
Add inventory system
83f4c73
Raw
History Blame Contribute Delete
18 kB
import gradio as gr
import json
import os
import uuid
import time
import re
from openai import OpenAI
from huggingface_hub import HfApi
from huggingface_hub import InferenceClient
from datasets import Dataset, load_dataset
from game.player import create_player
from game.turn_engine import play_turn
from data.save_system import save_to_hf, load_from_hf, load_and_enter_game
from game.vlm_engine import extract_scene_data, generate_entity_image, generate_player_avatar
from game.model_config import get_available_models, get_model_display_name, LANGUAGE_ROUTING
print("--- DEBUG INFO [APP]: APP STARTING...")
TEXT_MODEL_NAME = "meta-llama/Meta-Llama-3-8B-Instruct"
IMAGE_MODEL_NAME = "black-forest-labs/FLUX.1-schnell"
IMAGE_ROUTER_URL = f"https://router.huggingface.co/hf-inference/models/{IMAGE_MODEL_NAME}"
token = os.environ.get("HF_TOKEN")
if token is None:
raise ValueError("HF_TOKEN not set")
api = HfApi(token=token)
username = api.whoami()["name"]
text_client = OpenAI(
base_url="https://router.huggingface.co/v1",
api_key=token
)
image_client = InferenceClient(
model=IMAGE_ROUTER_URL,
token=token
)
DATASET_REPO = f"{username}/ai-rpg-saves"
# ----------------------------
# SYSTEM_PROMPT
# ----------------------------
prompt_file_path = "prompt/default_system.txt"
if os.path.exists(prompt_file_path):
with open(prompt_file_path, "r") as f:
SYSTEM_PROMPT = f.read()
else:
print("--- DEBUG ERROR [APP]:SYSTEM_PROMPT FILE DOES NOT EXIST")
# ----------------------------
# UI
# ----------------------------
# Dropdown için model listesini model_config'den al (hardcode yok)
ALL_MODELS = get_available_models()
with gr.Blocks(theme=gr.themes.Soft()) as demo:
player_state = gr.State("{}")
remaining_points = gr.State(10)
# ---------------- CHARACTER ----------------
with gr.Column(visible=True) as char_screen:
gr.Markdown("## Create Your Character")
name = gr.Textbox(label="Name")
gender = gr.Radio(["Male", "Female", "Non-binary"], label="Gender")
appearance = gr.Textbox(label="Appearance (Optional)", placeholder="e.g. hooded, scarred face, glowing cybernetic eye")
language = gr.Dropdown(
choices=["English", "Deutsch", "Español", "Français", "Türkçe"],
value="English",
label="Game Language"
)
# YENİ: Avatar Generate Butonu
with gr.Row():
gen_avatar_btn = gr.Button("🎲 Generate Avatar", variant="secondary")
# Mevcut avatar yükleme alanı. type="filepath" olduğu için üretilen görselin yolunu da kolayca gösterebiliriz.
avatar = gr.Image(type="filepath", label="Avatar (Upload or Generate)")
#start_btn = gr.Button("Continue")
with gr.Row():
start_btn = gr.Button("Continue", variant="primary")
load_char_btn = gr.Button("📂 Load Saved Game", variant="secondary")
# ---------------- STATS ----------------
with gr.Column(visible=False) as stat_screen:
gr.Markdown("## Distribute 10 Points")
remaining_text = gr.Markdown("### Points: 10")
stats = {}
buttons_pozitive = {}
buttons_negative = {}
stat_list = ["strength", "agility", "intelligence", "charisma", "willpower"]
for s in stat_list:
with gr.Row():
stats[s] = gr.Number(value=5, label=s.capitalize(), interactive=False)
buttons_pozitive[s] = gr.Button(f"+ {s}")
buttons_negative[s] = gr.Button(f"- {s}")
finish_stats = gr.Button("Start Game", variant="primary")
# ---------------- GAME ----------------
with gr.Column(visible=False) as game_screen:
with gr.Row():
# --- SOL PANEL: KARAKTER KAĞIDI VE SİSTEMLER (scale=1) ---
with gr.Column(scale=1):
with gr.Tabs():
# 1. SEKME: PROFİL VE ALIGNMENT
with gr.TabItem("👤 Profile"):
with gr.Row():
profile_pic = gr.Image(height=120, width=120, interactive=False, show_label=False)
profile_info = gr.Markdown("### Player Name")
gr.Markdown("---")
gr.Markdown("#### Alignment")
light_dark_slider = gr.Slider(-100, 100, value=0, label="Dark ↔ Light", interactive=False)
order_chaos_slider = gr.Slider(-100, 100, value=0, label="Chaos ↔ Order", interactive=False)
# 2. SEKME: STATLAR
with gr.TabItem("📊 Stats"):
stat_display = gr.Markdown("Loading stats...")
# 3. SEKME: ENVANTER
with gr.TabItem("🎒 Inventory"):
inventory_md = gr.Markdown(label="Inventory")
inventory_gallery = gr.Gallery(label="Items", columns=3, height="auto")
#inventory_display = gr.Markdown("*Your pockets are currently empty...*\n\n**Credits:** 0 🪙")
# 4. SEKME: JOURNAL
with gr.TabItem("📖 Journal"):
journal_display = gr.Markdown("*Your story has just begun... No entries yet.*")
gr.Markdown("---")
# YENİ: Model Seçici — model_config'deki tüm modeller buraya otomatik yükleniyor
gr.Markdown("#### 🤖 LLM Model")
gr.Markdown(
"*When 'Auto' is selected, the most suitable model according to your game language is automatically selected.*",
elem_id="model_hint"
)
model_selector = gr.Dropdown(
choices=["Auto (Language Routing)"] + ALL_MODELS,
value="Auto (Language Routing)",
label="Active Model",
interactive=True,
)
# KAYIT SİSTEMİ (Sol panelin en altında)
gr.Markdown("---")
with gr.Row():
save_btn = gr.Button("Save ☁️")
load_btn = gr.Button("Load ⬇️")
save_status = gr.Markdown()
# --- ORTA PANEL: HİKAYE VE AKSİYON (scale=3) ---
with gr.Column(scale=3):
scene_image = gr.Image(label="Current Scene", height=300, interactive=False)
chatbot = gr.Chatbot(height=400, show_label=False, type="tuples")
with gr.Row():
choice1 = gr.Button("1", variant="secondary")
choice2 = gr.Button("2", variant="secondary")
choice3 = gr.Button("3", variant="secondary")
choice4 = gr.Button("4", variant="secondary")
choice5 = gr.Button("5", variant="secondary")
with gr.Row():
custom_input = gr.Textbox(label="What do you do?", placeholder="Type your action here...", scale=4)
send_btn = gr.Button("Send Action", variant="primary", scale=1)
# --- SAĞ PANEL: NPC VE EŞYALAR (scale=1) ---
with gr.Column(scale=1):
gr.Markdown("### 👁️ Current Encounter")
npc_image = gr.Image(label="NPC / Enemy", height=200, interactive=False)
gr.Markdown("### 🎁 Loot / Item")
item_image = gr.Image(label="Discovered Item", height=200, interactive=False)
# --- AVATAR ÜRETİM MANTIĞI ---
def handle_avatar_generation(n, g, app):
# UI'ı kilitleyip üretimi başlatıyoruz
if not n:
n = "Unknown Drifter" # İsim girilmemişse varsayılan
# image_client zaten app.py'de tanımlı
avatar_path = generate_player_avatar(image_client, n, g, app)
return avatar_path
# Butona tıklandığında loading animasyonu ile birlikte görseli üret ve Image component'ine bas
gen_avatar_btn.click(
handle_avatar_generation,
inputs=[name, gender, appearance],
outputs=avatar,
api_name=False
)
# ---------------- LOGIC ----------------
def go_stats(n, g, a, l):
print(f"\n--- DEBUG INFO [APP]: Creating new player: {n} | Gender: {g} | Lang: {l} ---")
state = create_player(n, g, a, l)
return json.dumps(state), gr.update(visible=False), gr.update(visible=True)
start_btn.click(
go_stats,
inputs=[name, gender, avatar, language],
outputs=[player_state, char_screen, stat_screen],
api_name=False
)
def load_char_wrapper():
return load_and_enter_game(DATASET_REPO, from_char_screen=True)
load_char_btn.click(
load_char_wrapper,
inputs=[], # ← DATASET_REPO geçme! Wrapper içinde kullanılıyor
outputs= [
player_state, chatbot,
char_screen, stat_screen, game_screen, # ← char_screen eklendi
profile_pic, profile_info,
light_dark_slider, order_chaos_slider,
stat_display, scene_image, model_selector,
],
api_name=False
)
for s in stats:
def make_callback_pozitive(stat_name):
def handler(state_json, remaining):
state = json.loads(state_json)
if remaining <= 0:
return state_json, remaining, f"### Points: {remaining}", state[stat_name]
state[stat_name] += 1
remaining -= 1
return json.dumps(state), remaining, f"### Points: {remaining}", state[stat_name]
return handler
def make_callback_negative(stat_name):
def handler(state_json, remaining):
state = json.loads(state_json)
if state[stat_name] <= 0:
return state_json, remaining, f"### Points: {remaining}", state[stat_name]
state[stat_name] -= 1
remaining += 1
return json.dumps(state), remaining, f"### Points: {remaining}", state[stat_name]
return handler
buttons_pozitive[s].click(
make_callback_pozitive(s),
inputs=[player_state, remaining_points],
outputs=[player_state, remaining_points, remaining_text, stats[s]],
api_name=False
)
buttons_negative[s].click(
make_callback_negative(s),
inputs=[player_state, remaining_points],
outputs=[player_state, remaining_points, remaining_text, stats[s]],
api_name=False
)
def start_game(state_json):
state = json.loads(state_json)
# Statları şık bir metin formatına çeviriyoruz
stats_md = f"""
**Strength:** {state['strength']}
**Agility:** {state['agility']}
**Intelligence:** {state['intelligence']}
**Charisma:** {state['charisma']}
**Willpower:** {state['willpower']}
"""
return (
gr.update(visible=False),
gr.update(visible=True),
state["avatar"],
f"### {state['name']}",
state["light_dark"],
state["order_chaos"],
stats_md, # Yeni eklediğimiz stat ekranı verisi
None
)
def _resolve_override(model_selector_value):
"""
'Auto' seçiliyse None döner (model_config dil routing'i devreye girer).
Belirli bir model seçilmişse o model ID'sini döner.
"""
if model_selector_value == "Auto (Language Routing)":
return None
return model_selector_value
def play_turn_wrapper(user_input, history, state_json, model_sel):
print(f"\n--- DEBUG INFO [APP]: Turn triggered! Action: '{user_input}' ---")
override = _resolve_override(model_sel)
yield from play_turn(
user_input,
history,
state_json,
text_client,
image_client,
TEXT_MODEL_NAME,
SYSTEM_PROMPT,
model_override=override
)
def first_turn(history, state_json, model_sel):
state = json.loads(state_json)
lang = state.get("language", "English")
#choices=["English", "Deutsch", "Español", "Français", "Türkçe"],
opening_prompts = {
"Deutsch": "Starte das Abenteuer. Beschreibe meine Umgebung und gib mir 5 Optionen.",
"Español": "Comienza la aventura. Describe mi entorno y dame 5 opciones.",
"Français": "Commence l'aventure. Décris mon environnement et donne-moi 5 choix.",
"Türkçe": "Maceraya başla. Çevremdeki ortamı anlat ve bana 5 seçenek sun.",
}
opening = opening_prompts.get(lang, "Start the adventure. Describe my surroundings and give me 5 choices.")
override = _resolve_override(model_sel)
yield from play_turn(opening, history, state_json, text_client, image_client, TEXT_MODEL_NAME, SYSTEM_PROMPT, model_override=override)
ui_components = [choice1, choice2, choice3, choice4, choice5, custom_input, send_btn]
def lock_ui():
# İşlem başlarken her şeyi pasif (tıklanamaz) hale getir
return [gr.update(interactive=False) for _ in ui_components]
def unlock_ui():
# İşlem bitince her şeyi aktif hale getir ve Textbox'ın içini temizle (index 5)
updates = [gr.update(interactive=True) for _ in ui_components]
updates[5] = gr.update(interactive=True, value="")
return updates
# --- İLK TUR (OYUN BAŞLANGICI) ---
finish_stats.click(
start_game,
inputs=player_state,
outputs=[
stat_screen, game_screen,
profile_pic, profile_info,
light_dark_slider, order_chaos_slider,
stat_display, scene_image
],
api_name=False
).then(
lock_ui,
outputs=ui_components,
api_name=False
).then(
first_turn,
inputs=[chatbot, player_state, model_selector], # model_selector eklendi
outputs=[
chatbot, player_state,
light_dark_slider, order_chaos_slider,
scene_image, npc_image, item_image,
inventory_md, inventory_gallery,
choice1, choice2, choice3, choice4, choice5
],
api_name=False
).then(
unlock_ui,
outputs=ui_components,
api_name=False
)
# --- SERBEST METİN GİRİŞİ ---
send_btn.click(
lock_ui,
outputs=ui_components,
api_name=False
).then(
play_turn_wrapper,
inputs=[custom_input, chatbot, player_state, model_selector], # model_selector eklendi
outputs=[
chatbot, player_state,
light_dark_slider, order_chaos_slider,
scene_image, npc_image, item_image,
inventory_md, inventory_gallery,
choice1, choice2, choice3, choice4, choice5
],
api_name=False
).then(
unlock_ui,
outputs=ui_components,
api_name=False
)
# --- SEÇENEK BUTONLARI ---
choice_outputs = [chatbot, player_state, light_dark_slider, order_chaos_slider, scene_image, npc_image, item_image]
choice_buttons = [choice1, choice2, choice3, choice4, choice5]
for btn in choice_buttons:
btn.click(
lock_ui,
outputs=ui_components,
api_name=False
).then(
fn=play_turn_wrapper,
# Butonun kendisini inputs'a ekliyoruz, böylece metni gönderilir
inputs=[btn, chatbot, player_state, model_selector],
outputs=[
chatbot, player_state,
light_dark_slider, order_chaos_slider,
scene_image, npc_image, item_image,
inventory_md, inventory_gallery,
choice1, choice2, choice3, choice4, choice5
],
api_name=False
).then(
unlock_ui,
outputs=ui_components,
api_name=False
)
def save_wrapper(state_json, history_list, model_sel):
state = json.loads(state_json)
state["saved_model"] = model_sel # "Auto (Language Routing)" veya seçilen model ID
return save_to_hf(DATASET_REPO, json.dumps(state), json.dumps(history_list))
def load_ingame_wrapper():
return load_and_enter_game(DATASET_REPO, from_char_screen=False)
save_btn.click(
save_wrapper,
inputs=[player_state, chatbot, model_selector],
outputs=save_status,
api_name=False
)
#load_btn.click(
# load_wrapper,
# outputs=[player_state, chatbot],
# api_name=False
#)
# Ortak outputs listesi (artık char_screen de var)
#save_load_outputs = [
# player_state, chatbot,
# char_screen, stat_screen, game_screen, # ← char_screen eklendi
# profile_pic, profile_info,
# light_dark_slider, order_chaos_slider,
# stat_display, scene_image, model_selector
#]
load_btn.click(
load_ingame_wrapper,
inputs=[], # ← aynı şekilde
outputs= [
player_state, chatbot,
char_screen, stat_screen, game_screen, # ← char_screen eklendi
profile_pic, profile_info,
light_dark_slider, order_chaos_slider,
stat_display, scene_image, model_selector,
],
api_name=False
)
demo.launch(server_name="0.0.0.0", server_port=7860, show_error=True)