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)