import gradio as gr import json import os import uuid import time import re from openai import OpenAI from huggingface_hub import HfApi, InferenceClient from datasets import Dataset, load_dataset from game.player import create_player, create_player_from_npc 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 from game.image_model_config import get_available_image_models, get_image_model_display_name from game.constants import TRAIT_CHOICES, ASPIRATION_CHOICES, TRAIT_INFO, ASPIRATION_INFO from game.npc_registry import get_playable_npcs from systems.reputation_system import format_reputation_for_ui from systems.inventory_system import get_inventory_markdown, get_inventory_images from systems.relationship_system import get_relationships_markdown from systems.house_points_system import format_house_points_for_ui from systems.spell_system import get_known_spells_markdown 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( provider="fal-ai", api_key=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") # Dropdown için llm model listesini model_config'den al (hardcode yok) ALL_MODELS = get_available_models() # Dropdown için image model listesini de registry'den al IMAGE_ALL_MODELS = get_available_image_models() # ---------------------------- # CANONICAL CHARACTER CONSTANTS # ---------------------------- HOUSE_EMOJIS = { "Gryffindor": "🦁", "Slytherin": "🐍", "Ravenclaw": "🦅", "Hufflepuff": "🦡", } PLAYABLE_NPCS = get_playable_npcs() # {npc_id: npc_data} CANONICAL_CHOICES = [ ( f"{HOUSE_EMOJIS.get(npc['house'], '✨')} {npc['name']} — {npc['house']}", npc_id, ) for npc_id, npc in PLAYABLE_NPCS.items() ] # ---------------------------- # UI # ---------------------------- with gr.Blocks(theme=gr.themes.Soft()) as demo: player_state = gr.State("{}") remaining_points = gr.State(10) # ================== SCREEN 1 — 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") gr.Markdown("---") play_as_tab_btn = gr.Button( "🎭 Or: Play as a Canonical Character", variant="secondary", size="lg", ) # ================== SCREEN 1b — PLAY AS ================== with gr.Column(visible=False) as play_as_screen: gr.Markdown("## 🎭 Play as a Hogwarts Student") gr.Markdown( "*Step into the shoes of a canonical character. " "Stats, traits, and aspirations are pre-defined — jump straight into the story.*" ) canonical_selector = gr.Radio( choices=CANONICAL_CHOICES, label="Choose your character", value=None, ) canonical_preview = gr.Markdown( "*Select a character above to see their profile.*" ) canonical_language = gr.Dropdown( choices=["English", "Deutsch", "Español", "Français", "Türkçe"], value="English", label="Game Language", ) with gr.Row(): back_btn = gr.Button("← Create Character", variant="secondary") play_as_begin = gr.Button("▶ Begin Adventure", variant="primary") # ================== SCREEN 2 — 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}") gr.Markdown("---") gr.Markdown("### 🏰 Hogwarts Placement") gr.Markdown("*You are a transfer student from Durmstrang. Choose your house and year.*") house_select = gr.Radio( choices=["Gryffindor", "Hufflepuff", "Ravenclaw", "Slytherin"], label="House", value="Gryffindor", interactive=True ) year_select = gr.Radio( choices=[3, 4, 5, 6], label="Year", value=3, interactive=True ) finish_stats = gr.Button("Continue →", variant="primary") # ================== SCREEN 3 — TRAITS & ASPIRATIONS ================== with gr.Column(visible=False) as trait_screen: gr.Markdown("## ✨ Define Your Identity") gr.Markdown("*These choices will shape your entire adventure at Hogwarts.*") with gr.Row(): # --- Traits column --- with gr.Column(): gr.Markdown("### 🎭 Traits — Choose 3") gr.Markdown( "*Traits define your personality and affect dialogue, spells, and relationships.*" ) trait_selector = gr.CheckboxGroup( choices=TRAIT_CHOICES, label="", value=[] ) trait_count_md = gr.Markdown("**0 / 3 selected**") trait_desc_md = gr.Markdown("*Select traits above to see what they do.*") # --- Aspirations column --- with gr.Column(): gr.Markdown("### 🎯 Aspirations — Choose 2") gr.Markdown( "*Your life goals. Completing one unlocks a permanent special ability.*" ) aspiration_selector = gr.CheckboxGroup( choices=ASPIRATION_CHOICES, label="", value=[] ) aspiration_count_md = gr.Markdown("**0 / 2 selected**") aspiration_desc_md = gr.Markdown("*Select aspirations above to see their rewards.*") trait_error_md = gr.Markdown("", visible=False) begin_btn = gr.Button("⚡ Begin Your Hogwarts Adventure", variant="primary", size="lg") # ================== SCREEN 4 — 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("#### 🌐 Reputation") reputation_text = gr.Markdown("🫥 **Unassuming** (0)") gr.Markdown("---") gr.Markdown("#### 🏠 House Points") house_points_text = gr.Markdown("🏠 **+0** House Points — 😐 Unproven") 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.*") # 5. SEKME: RELATIONS with gr.TabItem("💞 Relations"): relations_display = gr.Markdown( "*You haven't gotten close to anyone yet. Relationships form as " "you meet and interact with people at Hogwarts.*" ) # 6. SEKME: SPELLS with gr.TabItem("✨ Spells"): known_spells_display = gr.Markdown( "*You haven't learned any spells yet. Attend a class, study a book, " "or ask someone to teach you one.*" ) 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 best model for your game language is chosen automatically.*", elem_id="model_hint" ) model_selector = gr.Dropdown( choices=["Auto (Language Routing)"] + ALL_MODELS, value="Auto (Language Routing)", label="Active Model", interactive=True, ) # YENİ: Image Model Seçici — image_model_config'deki tüm modeller otomatik yükleniyor gr.Markdown("#### 🎨 Image Model") image_model_selector = gr.Dropdown( choices=["Auto (Recommended)"] + IMAGE_ALL_MODELS, value="Auto (Recommended)", label="Active Image 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", height=200, interactive=False) gr.Markdown("### 🎁 Loot / Item") item_image = gr.Image(label="Discovered Item", height=200, interactive=False) # ---------------------------- # LOGIC FUNCTIONS # ---------------------------- 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="") # clear text input return updates # Avatar def handle_avatar_generation(n, g, app, img_model_sel): # UI'ı kilitleyip üretimi başlatıyoruz if not n: n = "Unknown Drifter" # İsim girilmemişse varsayılan img_override = _resolve_image_override(img_model_sel) # image_client zaten app.py'de tanımlı avatar_path = generate_player_avatar(image_client, n, g, app, model_override=img_override) return avatar_path # ---------------------------- # CANONICAL CHARACTER HELPERS # ---------------------------- def update_canonical_preview(npc_id): """Updates the character preview card when a canonical NPC is selected.""" if not npc_id: return "*Select a character above to see their profile.*" npc = PLAYABLE_NPCS.get(npc_id) if not npc: return "*Character not found.*" house_emoji = HOUSE_EMOJIS.get(npc.get("house", ""), "✨") traits_str = " · ".join(npc.get("traits", [])) asps_str = " · ".join(npc.get("aspirations", [])) stats = npc.get("starting_stats", {}) return ( f"### {house_emoji} {npc['name']}\n" f"**{npc['house']} · Year {npc['canonical_year']}**\n\n" f"*{npc.get('play_as_description', '')}*\n\n" f"**Traits:** {traits_str} \n" f"**Aspirations:** {asps_str} \n\n" f"**STR** {stats.get('strength',5)} · " f"**AGI** {stats.get('agility',5)} · " f"**INT** {stats.get('intelligence',5)} · " f"**CHA** {stats.get('charisma',5)} · " f"**WIL** {stats.get('willpower',5)}" ) def go_game_canonical(npc_id, lang): """ Creates the player state from a canonical NPC and prepares the game screen. Skips stat distribution and trait selection entirely. Returns 13 UI updates matching GO_CANONICAL_OUTPUTS. """ if not npc_id: return tuple([gr.update()] * 13) npc = PLAYABLE_NPCS.get(npc_id) if not npc: return tuple([gr.update()] * 13) state = create_player_from_npc(npc_id, npc, lang) traits_str = " · ".join(state.get("traits", [])) asps_str = " · ".join(state.get("aspirations", [])) stats_md = ( f"**House:** {state['house']} · **Year:** {state['year']} \n" f"**Traits:** {traits_str} \n" f"**Aspirations:** {asps_str} \n\n---\n" f"**Strength:** {state['strength']} \n" f"**Agility:** {state['agility']} \n" f"**Intelligence:** {state['intelligence']} \n" f"**Charisma:** {state['charisma']} \n" f"**Willpower:** {state['willpower']} " ) return ( json.dumps(state), # player_state gr.update(visible=False), # play_as_screen → hide gr.update(visible=True), # game_screen → show state.get("avatar"), # profile_pic f"### {state['name']}\n*Year {state['year']} · {state['house']}*", # profile_info state["light_dark"], # light_dark_slider state["order_chaos"], # order_chaos_slider format_reputation_for_ui(0), # reputation_text format_house_points_for_ui(0), # house_points_text stats_md, # stat_display None, # scene_image (cleared) get_inventory_markdown(state), # inventory_md get_inventory_images(state), # inventory_gallery get_relationships_markdown(state), # relations_display get_known_spells_markdown(state), # known_spells_display ) # 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, image_model_selector], outputs=avatar, api_name=False ) # ---- Screen 1 → Screen 2 ---- def go_stats(n, g, a, l): print(f"\n--- DEBUG INFO [APP]: Creating 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, profile_pic, profile_info, light_dark_slider, order_chaos_slider, reputation_text, house_points_text, stat_display, scene_image, model_selector, image_model_selector, inventory_md, inventory_gallery, relations_display, known_spells_display, ], api_name=False ) # ---- Stat +/- buttons ---- 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 ) # ---- Screen 2 → Screen 3 ---- def go_to_trait_screen(state_json, house_val, year_val): state = json.loads(state_json) state["house"] = house_val state["year"] = int(year_val) return json.dumps(state), gr.update(visible=False), gr.update(visible=True) finish_stats.click( go_to_trait_screen, inputs=[player_state, house_select, year_select], outputs=[player_state, stat_screen, trait_screen], api_name=False ) # ---- Trait / aspiration live preview ---- def update_trait_display(selected): count = len(selected) count_text = f"**{count} / 3 selected**" + (" ✅" if count == 3 else "") desc = ( "\n\n".join( f"**{TRAIT_INFO[n]['emoji']} {n}** — {TRAIT_INFO[n]['short']}" for n in selected if n in TRAIT_INFO ) if selected else "*Select traits above to see what they do.*" ) return count_text, desc def update_aspiration_display(selected): count = len(selected) count_text = f"**{count} / 2 selected**" + (" ✅" if count == 2 else "") desc = ( "\n\n".join( f"**{ASPIRATION_INFO[n]['emoji']} {n}** — {ASPIRATION_INFO[n]['short']}" for n in selected if n in ASPIRATION_INFO ) if selected else "*Select aspirations above to see their rewards.*" ) return count_text, desc trait_selector.change( update_trait_display, inputs=[trait_selector], outputs=[trait_count_md, trait_desc_md], api_name=False ) aspiration_selector.change( update_aspiration_display, inputs=[aspiration_selector], outputs=[aspiration_count_md, aspiration_desc_md], api_name=False ) # ---- Screen 3 → Screen 4 ---- def confirm_traits_and_start(state_json, traits, aspirations): errors = [] if len(traits) != 3: errors.append(f"⚠️ Select **exactly 3 traits** ({len(traits)} selected).") if len(aspirations) != 2: errors.append(f"⚠️ Select **exactly 2 aspirations** ({len(aspirations)} selected).") if errors: # Validation failed — stay on trait_screen, show errors, don't change anything else return ( state_json, gr.update(), gr.update(), # trait_screen, game_screen unchanged gr.update(), gr.update(), # profile_pic, profile_info unchanged gr.update(), gr.update(), # sliders unchanged gr.update(), gr.update(), gr.update(), # stat_display, scene_image, reputation unchanged gr.update(value="\n\n".join(errors), visible=True), ) state = json.loads(state_json) state["traits"] = traits state["aspirations"] = aspirations stats_md = ( f"**House:** {state['house']} · **Year:** {state['year']} \n" f"**Traits:** {' · '.join(traits)} \n" f"**Aspirations:** {' · '.join(aspirations)} \n\n---\n" f"**Strength:** {state['strength']} \n" f"**Agility:** {state['agility']} \n" f"**Intelligence:** {state['intelligence']} \n" f"**Charisma:** {state['charisma']} \n" f"**Willpower:** {state['willpower']} " ) return ( json.dumps(state), gr.update(visible=False), # trait_screen → hide gr.update(visible=True), # game_screen → show state["avatar"], # profile_pic f"### {state['name']}\n*Year {state['year']} · {state['house']}*", state["light_dark"], state["order_chaos"], stats_md, None, # scene_image (cleared; first scene arrives via first_turn) gr.update(visible=False), # hide any previous error ) # ---- Gameplay helpers ---- 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 _resolve_image_override(image_model_selector_value): """ 'Auto' seçiliyse None döner (image_model_config'teki varsayılan devreye girer). Belirli bir görsel model seçilmişse o model ID'sini döner. """ if image_model_selector_value == "Auto (Recommended)": return None return image_model_selector_value def play_turn_wrapper(user_input, history, state_json, model_sel, image_model_sel): print(f"\n--- DEBUG INFO [APP]: Turn triggered! Action: '{user_input}' ---") override = _resolve_override(model_sel) image_override = _resolve_image_override(image_model_sel) yield from play_turn( user_input, history, state_json, text_client, image_client, TEXT_MODEL_NAME, SYSTEM_PROMPT, model_override=override, image_model_override = image_override ) def first_turn(history, state_json, model_sel, image_model_sel): state = json.loads(state_json) # Guard: don't run if trait/aspiration selection was invalid (validation failed) if len(state.get("traits", [])) != 3 or len(state.get("aspirations", [])) != 2: print("--- DEBUG INFO [APP]: first_turn guard — traits not confirmed, skipping.") yield ( history, state_json, state.get("light_dark", 0), state.get("order_chaos", 0), gr.update(), # reputation_text gr.update(), gr.update(), gr.update(), # scene, npc, item gr.update(), gr.update(), # inventory_md, inventory_gallery gr.update(), # relations_display ← EKSİKTİ gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), # 5 buton ) return lang = state.get("language", "English") if state.get("is_canonical_character"): # --- Canonical character opening --- name = state.get("name", "") house = state.get("house", "") year = state.get("year", 3) canonical_openings = { "English": ( f"Start the adventure. I am {name}, a Year {year} {house} student at Hogwarts. " f"This is a peaceful school year — no grand threat. Begin with a natural, " f"typical morning scene that fits exactly who I am. Give me 5 choices." ), "Deutsch": ( f"Starte das Abenteuer. Ich bin {name}, Schüler/in in {house}, {year}. Jahrgang in Hogwarts. " f"Es ist ein ruhiges Schuljahr. Beginne mit einer typischen Morgenszene, die zu " f"meinem Charakter passt. Gib mir 5 Optionen." ), "Español": ( f"Comienza la aventura. Soy {name}, estudiante de {house} en el año {year} en Hogwarts. " f"Es un año tranquilo. Empieza con una escena matutina típica para mi personaje. " f"Dame 5 opciones." ), "Français": ( f"Commence l'aventure. Je suis {name}, étudiant(e) en {house} en {year}e année à Poudlard. " f"C'est une année tranquille. Commence par une scène matinale typique pour mon personnage. " f"Donne-moi 5 choix." ), "Türkçe": ( f"Maceraya başla. Ben {name}'yim, Hogwarts'ta {year}. sınıf {house} öğrencisiyim. " f"Huzurlu bir okul yılı. Karakterime tamamen uygun tipik bir sabah sahnesiyle başla. " f"Bana 5 seçenek sun." ), } opening = canonical_openings.get(lang, canonical_openings["English"]) else: # --- Custom (Durmstrang transfer) opening --- house = state.get("house", "Gryffindor") year = state.get("year", 3) 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, f"Start the adventure. I am a transfer student from Durmstrang, newly sorted into " f"{house} in Year {year} at Hogwarts. Begin with the Sorting Hat ceremony confirming " f"my placement, and my first steps into the {house} common room. Give me 5 choices." ) override = _resolve_override(model_sel) image_override = _resolve_image_override(image_model_sel) yield from play_turn( opening, history, state_json, text_client, image_client, TEXT_MODEL_NAME, SYSTEM_PROMPT, model_override=override, image_model_override=image_override ) # ---------------------------- # TURN OUTPUTS (shared list — used by all play_turn event chains) # Order must match turn_engine.py play_turn yield exactly. # ---------------------------- TURN_OUTPUTS = [ chatbot, player_state, light_dark_slider, order_chaos_slider, reputation_text, # ← NEW (position 5) house_points_text, scene_image, npc_image, item_image, inventory_md, inventory_gallery, relations_display, # ← NEW (Relations tab) known_spells_display, # ← NEW (Spells tab) choice1, choice2, choice3, choice4, choice5, ] # ---------------------------- # EVENT CHAINS # ---------------------------- # ---- PLAY AS: char_screen → play_as_screen ---- play_as_tab_btn.click( lambda: (gr.update(visible=False), gr.update(visible=True)), outputs=[char_screen, play_as_screen], api_name=False, ) # ---- PLAY AS: play_as_screen → char_screen (back) ---- back_btn.click( lambda: (gr.update(visible=True), gr.update(visible=False)), outputs=[char_screen, play_as_screen], api_name=False, ) # ---- PLAY AS: NPC selector → live preview ---- canonical_selector.change( update_canonical_preview, inputs=[canonical_selector], outputs=[canonical_preview], api_name=False, ) # ---- PLAY AS: Begin → game_screen (skip stat/trait screens) ---- GO_CANONICAL_OUTPUTS = [ player_state, play_as_screen, game_screen, profile_pic, profile_info, light_dark_slider, order_chaos_slider, reputation_text, house_points_text, stat_display, scene_image, inventory_md, inventory_gallery, relations_display, known_spells_display, ] play_as_begin.click( go_game_canonical, inputs=[canonical_selector, canonical_language], outputs=GO_CANONICAL_OUTPUTS, api_name=False, ).then( lock_ui, outputs=ui_components, api_name=False, ).then( first_turn, inputs=[chatbot, player_state, model_selector, image_model_selector], outputs=TURN_OUTPUTS, api_name=False, ).then( unlock_ui, outputs=ui_components, api_name=False, ) # Screen 3 → begin adventure begin_btn.click( confirm_traits_and_start, inputs=[player_state, trait_selector, aspiration_selector], outputs=[ player_state, trait_screen, game_screen, profile_pic, profile_info, light_dark_slider, order_chaos_slider, stats_md_out := stat_display, scene_image, trait_error_md, ], api_name=False ).then( lock_ui, outputs=ui_components, api_name=False ).then( first_turn, inputs=[chatbot, player_state, model_selector, image_model_selector], outputs=TURN_OUTPUTS, 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, image_model_selector], # model_selector eklendi outputs=TURN_OUTPUTS, 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( play_turn_wrapper, # Butonun kendisini inputs'a ekliyoruz, böylece metni gönderilir inputs=[btn, chatbot, player_state, model_selector, image_model_selector], outputs=TURN_OUTPUTS, api_name=False ).then( unlock_ui, outputs=ui_components, api_name=False ) # Save / Load def save_wrapper(state_json, history_list, model_sel, image_model_sel): state = json.loads(state_json) state["saved_model"] = model_sel # "Auto (Language Routing)" veya seçilen model ID state["saved_image_model"] = image_model_sel # "Auto (Recommended)" veya seçilen image 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, image_model_selector], outputs=save_status, api_name=False ) load_btn.click( load_ingame_wrapper, inputs=[], outputs= [ player_state, chatbot, char_screen, stat_screen, game_screen, profile_pic, profile_info, light_dark_slider, order_chaos_slider, reputation_text, house_points_text, stat_display, scene_image, model_selector, image_model_selector, inventory_md, inventory_gallery, relations_display, known_spells_display, ], api_name=False ) demo.launch(server_name="0.0.0.0", server_port=7860, show_error=True)