import gradio as gr
from css import custom_css, loading_css_styles
from audio.audio_generator import (
update_audio,
cleanup_music_session,
)
import logging
from agent.runner import process_step
import uuid
from game_constructor import (
SETTING_SUGGESTIONS,
CHARACTER_SUGGESTIONS,
GENRE_OPTIONS,
load_setting_suggestion,
load_character_suggestion,
start_game_with_settings,
)
from app_description import app_description
CONCURRENCY_LIMIT = 10000
logger = logging.getLogger(__name__)
async def return_to_constructor(user_hash: str):
"""Return to the constructor and reset user state and audio."""
from agent.redis_state import reset_user_state
await reset_user_state(user_hash)
await cleanup_music_session(user_hash)
return (
gr.update(visible=False), # loading_indicator
gr.update(visible=True), # constructor_interface
gr.update(visible=False), # game_interface
gr.update(visible=False), # error_message
)
num_visits = 0
async def generate_user_hash():
hash = str(uuid.uuid4())
global num_visits
num_visits += 1
logger.info(f"Generated user hash: {hash}, num_visits: {num_visits}")
return gr.update(value=hash)
async def update_scene(user_hash: str, choice):
logger.info(f"Updating scene with choice: {choice}")
if not isinstance(choice, str):
return gr.update(), gr.update(), gr.update(), gr.update()
result = await process_step(
user_hash=user_hash,
step="choose",
choice_text=choice,
)
if result.get("game_over"):
ending = result["ending"]
ending_text = (
ending.get("description") or ending.get("condition", "")
) + "\n[THE END]"
ending_image = result.get("image")
return (
gr.update(value=ending_text),
gr.update(value=ending_image),
gr.Radio(choices=[], label="", value=None, visible=False),
gr.update(value="", visible=False),
)
scene = result["scene"]
return (
scene["description"],
scene.get("image", ""),
gr.Radio(
choices=[ch["text"] for ch in scene.get("choices", [])],
label="What do you choose? (select an option or write your own)",
value=None,
elem_classes=["choice-buttons"],
),
gr.update(value=""),
)
def update_preview(setting, name, age, background, personality, genre):
"""Update the configuration preview"""
if not any([setting, name, age, background, personality]):
return "Fill in the fields to see a preview..."
preview = f"""🌍 SETTING: {setting[:100]}{"..." if len(setting) > 100 else ""}
👤 CHARACTER: {name} (Age: {age})
📖 Background: {background}
💭 Personality: {personality}
🎭 GENRE: {genre}"""
return preview
async def start_game_with_music(
user_hash: str,
setting_desc: str,
char_name: str,
char_age: str,
char_background: str,
char_personality: str,
genre: str,
):
"""Start the game with custom settings and initialize music"""
yield (
gr.update(visible=True), # loading indicator
gr.update(), # constructor_interface
gr.update(), # game_interface
gr.update(), # error_message
gr.update(),
gr.update(),
gr.update(), # game components unchanged
gr.update(), # custom choice unchanged
)
# First, get the game interface updates
result = await start_game_with_settings(
user_hash,
setting_desc,
char_name,
char_age,
char_background,
char_personality,
genre,
)
yield result
with gr.Blocks(
theme="soft",
title="Game Constructor & Visual Novel",
css=custom_css + loading_css_styles,
) as demo:
# Fullscreen Loading Indicator (hidden by default)
with gr.Column(visible=False, elem_id="loading-indicator") as loading_indicator:
gr.HTML("
🚀 Starting your adventure...
")
ls_user_hash = gr.BrowserState("", "user_hash")
# Constructor Interface (visible by default)
with gr.Column(
visible=True, elem_id="constructor-interface", elem_classes=["constructor-page"]
) as constructor_interface:
with gr.Row():
app_description()
with gr.Row():
error_message = gr.Textbox(
label="⚠️ Error",
visible=False,
interactive=False,
elem_classes=["error-message"],
)
with gr.Row():
with gr.Column(scale=2):
# Setting Description Section
with gr.Group():
gr.Markdown("## 🌍 Setting Description")
setting_suggestions = gr.Dropdown(
choices=["Select a suggestion..."] + SETTING_SUGGESTIONS,
label="Quick Suggestions",
value="Select a suggestion...",
interactive=True,
)
setting_description = gr.Textbox(
label="Describe your game setting",
placeholder="Enter a detailed description of where your story takes place...",
lines=4,
max_lines=6,
)
# Character Description Section
with gr.Group():
gr.Markdown("## 👤 Character Description")
character_suggestions = gr.Dropdown(
choices=["None"]
+ [
f"{char['name']} - {char['background'][:50]}..."
for char in CHARACTER_SUGGESTIONS
],
label="Character Templates",
value="None",
interactive=True,
)
with gr.Row():
char_name = gr.Textbox(
label="Character Name",
placeholder="Enter character name...",
)
char_age = gr.Textbox(label="Age", placeholder="25")
char_background = gr.Textbox(
label="Background/Profession",
placeholder="Describe your character's background, profession, or role...",
lines=2,
)
char_personality = gr.Textbox(
label="Personality & Traits",
placeholder="Describe personality, quirks, motivations, fears...",
lines=2,
)
# Genre Selection Section
with gr.Group():
gr.Markdown("## 🎭 Genre & Style")
genre_selection = gr.Dropdown(
choices=GENRE_OPTIONS,
label="Choose your story genre",
value=GENRE_OPTIONS[0],
interactive=True,
)
with gr.Column(scale=1):
# Preview Section
with gr.Group():
gr.Markdown("## 📋 Configuration Preview")
preview_box = gr.Textbox(
label="Game Summary",
lines=8,
interactive=False,
placeholder="Fill in the fields to see a preview...",
)
with gr.Group():
gr.Markdown("## 🎮 Ready to Play?")
start_btn = gr.Button("▶️ Start Game", variant="primary", size="lg")
with gr.Column(visible=False, elem_id="game-interface") as game_interface:
back_btn = gr.Button(
"⬅️ Back to Constructor",
variant="secondary",
elem_id="back-btn",
)
# Audio component for background music
audio_out = gr.Audio(
autoplay=True, streaming=True, interactive=False, visible=False
)
# Background image (fullscreen)
with gr.Column(elem_classes=["image-container"]):
game_image = gr.Image(type="filepath", interactive=False, show_label=False)
# Overlay content (text and buttons)
with gr.Column(elem_classes=["overlay-content"]):
game_text = gr.Textbox(
label="",
interactive=False,
show_label=False,
elem_classes=["narrative-text"],
lines=3,
)
with gr.Column(elem_classes=["choice-area"]):
game_choices = gr.Radio(
choices=[],
label="What do you choose? (select an option or write your own)",
value=None,
elem_classes=["choice-buttons"],
)
custom_choice = gr.Textbox(
label="",
show_label=False,
placeholder="Type your option and press Enter",
lines=1,
elem_classes=["choice-input"],
)
# Event handlers for constructor interface
setting_suggestions.change(
fn=load_setting_suggestion,
inputs=[setting_suggestions],
outputs=[setting_description],
)
character_suggestions.change(
fn=load_character_suggestion,
inputs=[character_suggestions],
outputs=[char_name, char_age, char_background, char_personality],
)
# Update preview when any field changes
for component in [
setting_description,
char_name,
char_age,
char_background,
char_personality,
genre_selection,
]:
component.change(
fn=update_preview,
inputs=[
setting_description,
char_name,
char_age,
char_background,
char_personality,
genre_selection,
],
outputs=[preview_box],
)
# Interface switching handlers
start_btn.click(
fn=start_game_with_music,
inputs=[
ls_user_hash,
setting_description,
char_name,
char_age,
char_background,
char_personality,
genre_selection,
],
outputs=[
loading_indicator,
constructor_interface,
game_interface,
error_message,
game_text,
game_image,
game_choices,
custom_choice,
],
)
back_btn.click(
fn=return_to_constructor,
inputs=[ls_user_hash],
outputs=[
loading_indicator,
constructor_interface,
game_interface,
error_message,
],
)
game_choices.change(
fn=update_scene,
inputs=[ls_user_hash, game_choices],
outputs=[game_text, game_image, game_choices, custom_choice],
concurrency_limit=CONCURRENCY_LIMIT,
)
custom_choice.submit(
fn=update_scene,
inputs=[ls_user_hash, custom_choice],
outputs=[game_text, game_image, game_choices, custom_choice],
)
demo.unload(cleanup_music_session)
demo.load(
fn=generate_user_hash,
inputs=[],
outputs=[ls_user_hash],
)
ls_user_hash.change(
fn=update_audio,
inputs=[ls_user_hash],
outputs=[audio_out],
)
demo.queue(default_concurrency_limit=CONCURRENCY_LIMIT)
demo.launch(ssr_mode=False)