Spaces:
Running
Running
| 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) |