gsavin commited on
Commit
86b351a
·
0 Parent(s):

A new start

Browse files
.env.example ADDED
@@ -0,0 +1 @@
 
 
1
+ GEMINI_API_KEY=KEY
.gitattributes ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ *.7z filter=lfs diff=lfs merge=lfs -text
2
+ *.arrow filter=lfs diff=lfs merge=lfs -text
3
+ *.bin filter=lfs diff=lfs merge=lfs -text
4
+ *.bz2 filter=lfs diff=lfs merge=lfs -text
5
+ *.ckpt filter=lfs diff=lfs merge=lfs -text
6
+ *.ftz filter=lfs diff=lfs merge=lfs -text
7
+ *.gz filter=lfs diff=lfs merge=lfs -text
8
+ *.h5 filter=lfs diff=lfs merge=lfs -text
9
+ *.joblib filter=lfs diff=lfs merge=lfs -text
10
+ *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
+ *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
+ *.model filter=lfs diff=lfs merge=lfs -text
13
+ *.msgpack filter=lfs diff=lfs merge=lfs -text
14
+ *.npy filter=lfs diff=lfs merge=lfs -text
15
+ *.npz filter=lfs diff=lfs merge=lfs -text
16
+ *.onnx filter=lfs diff=lfs merge=lfs -text
17
+ *.ot filter=lfs diff=lfs merge=lfs -text
18
+ *.parquet filter=lfs diff=lfs merge=lfs -text
19
+ *.pb filter=lfs diff=lfs merge=lfs -text
20
+ *.pickle filter=lfs diff=lfs merge=lfs -text
21
+ *.pkl filter=lfs diff=lfs merge=lfs -text
22
+ *.pt filter=lfs diff=lfs merge=lfs -text
23
+ *.pth filter=lfs diff=lfs merge=lfs -text
24
+ *.rar filter=lfs diff=lfs merge=lfs -text
25
+ *.safetensors filter=lfs diff=lfs merge=lfs -text
26
+ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
+ *.tar.* filter=lfs diff=lfs merge=lfs -text
28
+ *.tar filter=lfs diff=lfs merge=lfs -text
29
+ *.tflite filter=lfs diff=lfs merge=lfs -text
30
+ *.tgz filter=lfs diff=lfs merge=lfs -text
31
+ *.wasm filter=lfs diff=lfs merge=lfs -text
32
+ *.xz filter=lfs diff=lfs merge=lfs -text
33
+ *.zip filter=lfs diff=lfs merge=lfs -text
34
+ *.zst filter=lfs diff=lfs merge=lfs -text
35
+ *tfevents* filter=lfs diff=lfs merge=lfs -text
.gitignore ADDED
@@ -0,0 +1,178 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+
6
+ # C extensions
7
+ *.so
8
+
9
+ # Distribution / packaging
10
+ .Python
11
+ build/
12
+ develop-eggs/
13
+ dist/
14
+ downloads/
15
+ eggs/
16
+ .eggs/
17
+ lib/
18
+ lib64/
19
+ parts/
20
+ sdist/
21
+ var/
22
+ wheels/
23
+ share/python-wheels/
24
+ *.egg-info/
25
+ .installed.cfg
26
+ *.egg
27
+ MANIFEST
28
+
29
+ # PyInstaller
30
+ # Usually these files are written by a python script from a template
31
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
32
+ *.manifest
33
+ *.spec
34
+
35
+ # Installer logs
36
+ pip-log.txt
37
+ pip-delete-this-directory.txt
38
+
39
+ # Unit test / coverage reports
40
+ htmlcov/
41
+ .tox/
42
+ .nox/
43
+ .coverage
44
+ .coverage.*
45
+ .cache
46
+ nosetests.xml
47
+ coverage.xml
48
+ *.cover
49
+ *.py,cover
50
+ .hypothesis/
51
+ .pytest_cache/
52
+ cover/
53
+
54
+ # Translations
55
+ *.mo
56
+ *.pot
57
+
58
+ # Django stuff:
59
+ *.log
60
+ local_settings.py
61
+ db.sqlite3
62
+ db.sqlite3-journal
63
+
64
+ # Flask stuff:
65
+ instance/
66
+ .webassets-cache
67
+
68
+ # Scrapy stuff:
69
+ .scrapy
70
+
71
+ # Sphinx documentation
72
+ docs/_build/
73
+
74
+ # PyBuilder
75
+ .pybuilder/
76
+ target/
77
+
78
+ # Jupyter Notebook
79
+ .ipynb_checkpoints
80
+
81
+ # IPython
82
+ profile_default/
83
+ ipython_config.py
84
+
85
+ # pyenv
86
+ # For a library or package, you might want to ignore these files since the code is
87
+ # intended to run in multiple environments; otherwise, check them in:
88
+ # .python-version
89
+
90
+ # pipenv
91
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
92
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
93
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
94
+ # install all needed dependencies.
95
+ #Pipfile.lock
96
+
97
+ # UV
98
+ # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
99
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
100
+ # commonly ignored for libraries.
101
+ #uv.lock
102
+
103
+ # poetry
104
+ # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
105
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
106
+ # commonly ignored for libraries.
107
+ # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
108
+ #poetry.lock
109
+
110
+ # pdm
111
+ # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
112
+ #pdm.lock
113
+ # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
114
+ # in version control.
115
+ # https://pdm.fming.dev/latest/usage/project/#working-with-version-control
116
+ .pdm.toml
117
+ .pdm-python
118
+ .pdm-build/
119
+
120
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
121
+ __pypackages__/
122
+
123
+ # Celery stuff
124
+ celerybeat-schedule
125
+ celerybeat.pid
126
+
127
+ # SageMath parsed files
128
+ *.sage.py
129
+
130
+ # Environments
131
+ .env
132
+ .venv
133
+ env/
134
+ venv/
135
+ ENV/
136
+ env.bak/
137
+ venv.bak/
138
+
139
+ # Spyder project settings
140
+ .spyderproject
141
+ .spyproject
142
+
143
+ # Rope project settings
144
+ .ropeproject
145
+
146
+ # mkdocs documentation
147
+ /site
148
+
149
+ # mypy
150
+ .mypy_cache/
151
+ .dmypy.json
152
+ dmypy.json
153
+
154
+ # Pyre type checker
155
+ .pyre/
156
+
157
+ # pytype static type analyzer
158
+ .pytype/
159
+
160
+ # Cython debug symbols
161
+ cython_debug/
162
+
163
+ # PyCharm
164
+ # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
165
+ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
166
+ # and can be added to the global gitignore or merged into this file. For a more nuclear
167
+ # option (not recommended) you can uncomment the following to ignore the entire idea folder.
168
+ #.idea/
169
+
170
+ # Ruff stuff:
171
+ .ruff_cache/
172
+
173
+ # PyPI configuration file
174
+ .pypirc
175
+ versions/
176
+
177
+ .gradio
178
+ generated/
README.md ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: Test
3
+ emoji: 🎮
4
+ colorFrom: yellow
5
+ colorTo: green
6
+ sdk: gradio
7
+ sdk_version: 5.32.0
8
+ app_file: src/main.py
9
+ pinned: false
10
+ license: mit
11
+ ---
12
+
13
+ # 🎮 LLMGameHub | gradio-mcp-hackaton
14
+
15
+ **LLMGameHub** is a next-generation playground where anyone can create, share, and play AI-powered games based on their own stories and texts.
16
+
17
+
18
+ https://github.com/user-attachments/assets/1ec276e2-ba95-4e85-b5b9-e8831703a52c
19
+
20
+
21
+ ## What is LLMGameHub?
22
+
23
+ LLMGameHub is an interactive platform that brings together the power of large language models and generative AI to let users co-create narrative games, visual novels, and interactive experiences in just a few clicks.
24
+ Whether you're a writer, gamer, or simply curious, you can turn your ideas into playable games — no coding required!
25
+
26
+ ## Key Features
27
+
28
+ - **User-driven Storytelling:**
29
+ Players build their own visual novel game by selecting key elements: setting, main character, and genre (e.g., horror, detective, romance, adventure).
30
+ - **Auto-generated Content:**
31
+ The platform dynamically generates engaging narratives, character descriptions, dialogue, and visual assets (images and short videos) tailored to the user's preferences.
32
+ - **Multi-Agent LLM System:**
33
+ Behind every game is a team of collaborating AI agents — each with their own roles — to create rich, believable, and creative worlds.
34
+ - **Short, Replayable Sessions:**
35
+ Each game is designed for quick, fun playthroughs (~5 minutes), making it perfect for demos and repeated experimentation.
36
+ - **Fast and Immersive:**
37
+ Instant feedback for user choices (responses in under 5 seconds) and high-quality assets generated in the background.
38
+ Atmospheric audio and multiple possible endings are included for full immersion.
39
+ - **Creative Freedom:**
40
+ Anyone can build a game from their own story idea or inspiration. Share your creation or play games made by others.
41
+
42
+ ## Why LLMGameHub?
43
+
44
+ We believe AI should be a creative partner. With LLMGameHub, you don't just play games — you shape them, experiment with genres, and explore what's possible when human imagination meets the latest in generative AI.
45
+ Bring your story to life — or let AI surprise you!
46
+
47
+ ## How does it work?
48
+
49
+ 1. **Start:** Choose a game genre, world setting, and main character.
50
+ 2. **Co-Create:** Answer a few simple questions or pick from suggestions.
51
+ 3. **Watch the Magic:** The AI agents generate a unique story, visuals, audio, and game flow based on your choices.
52
+ 4. **Play:** Dive into your own visual novel, make decisions, and explore different endings.
53
+ 5. **Share:** Save and share your favorite games with friends (feature coming soon).
54
+
55
+ ---
56
+
57
+ **LLMGameHub** — Create. Play. Imagine.
requirements.txt ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ gradio==5.32.0
2
+ google-genai==1.18.0
3
+ Pillow==10.1.0
4
+ requests==2.31.0
5
+ python-dotenv==1.0.0
6
+ asyncio
7
+ aiohttp==3.9.1
8
+ pygame==2.5.2
9
+ numpy
10
+ langchain==0.3.17
11
+ langchain-core==0.3.58
12
+ langchain-community==0.3.16
13
+ langchain-google-genai==2.1.4
14
+ pydantic-core==2.23.4
15
+ pydantic-settings==2.7.1
16
+ pydantic==2.9.2
src/agent/game_generator.py ADDED
File without changes
src/agent/llm.py ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from langchain_google_genai import ChatGoogleGenerativeAI
2
+ import logging
3
+ from config import settings
4
+
5
+ logger = logging.getLogger(__name__)
6
+
7
+ _google_api_keys_list = []
8
+ _current_google_key_idx = 0
9
+
10
+
11
+ def create_llm(temperature: float = settings.temperature, top_p: float = settings.top_p):
12
+ global _google_api_keys_list, _current_google_key_idx
13
+
14
+ if not _google_api_keys_list:
15
+ api_keys_str = settings.gemini_api_key.get_secret_value()
16
+ if api_keys_str:
17
+ _google_api_keys_list = [key.strip() for key in api_keys_str.split(',') if key.strip()]
18
+
19
+ if not _google_api_keys_list:
20
+ logger.error("Google API keys are not configured or are empty in settings.")
21
+ raise ValueError("Google API keys are not configured or are invalid for round-robin.")
22
+
23
+ if not _google_api_keys_list: # Safeguard, though previous block should handle it.
24
+ logger.error("No Google API keys available for round-robin.")
25
+ raise ValueError("No Google API keys available for round-robin.")
26
+
27
+ key_index_to_use = _current_google_key_idx
28
+ selected_api_key = _google_api_keys_list[key_index_to_use]
29
+
30
+ _current_google_key_idx = (key_index_to_use + 1) % len(_google_api_keys_list)
31
+
32
+ logger.info(f"Using Google API key at index {key_index_to_use} (ending with ...{selected_api_key[-4:] if len(selected_api_key) > 4 else selected_api_key}) for round-robin.")
33
+
34
+ return ChatGoogleGenerativeAI(
35
+ model="gemini-2.5-flash-preview-05-20",
36
+ google_api_key=selected_api_key,
37
+ temperature=temperature,
38
+ top_p=top_p,
39
+ thinking_budget=1024
40
+ )
41
+
42
+ def create_precise_llm():
43
+ return create_llm(temperature=0, top_p=1)
src/agent/llm_agent.py ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from agent.llm import create_llm
2
+ from pydantic import BaseModel, Field
3
+ from typing import Optional, List
4
+ import logging
5
+
6
+ logger = logging.getLogger(__name__)
7
+
8
+ class ChangeScene(BaseModel):
9
+ change_scene: bool = Field(description="Whether the scene should be changed")
10
+ scene_description: Optional[str] = None
11
+
12
+ class ChangeMusic(BaseModel):
13
+ change_music: bool = Field(description="Whether the music should be changed")
14
+ music_description: Optional[str] = None
15
+
16
+ class PlayerOption(BaseModel):
17
+ option_description: str = Field(description="The description of the option, Examples: [Change location] Go to the forest; [Say] Hello!")
18
+
19
+ class LLMOutput(BaseModel):
20
+ change_scene: ChangeScene
21
+ change_music: ChangeMusic
22
+ game_message: str = Field(description="The message to the player, Example: You entered the forest, and you see unknown scary creatures. What do you do?")
23
+ player_options: List[PlayerOption] = Field(description="The list of up to 3 options for the player to choose from.")
24
+
25
+ llm = create_llm().with_structured_output(LLMOutput)
26
+
27
+ async def process_user_input(input: str) -> LLMOutput:
28
+ """
29
+ Process user input and update the state.
30
+ """
31
+ logger.info(f"User's choice: {input}")
32
+
33
+ response: LLMOutput = await llm.ainvoke(input)
34
+
35
+ logger.info(f"LLM response: {response}")
36
+
37
+ return response
38
+
src/agent/llm_graph.py ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from agent.tools import available_tools
2
+ from agent.llm import create_llm
3
+ from langgraph.graph import MessagesState
4
+ class CustomState(MessagesState):
5
+ """Расширенное состояние графа."""
6
+
7
+
8
+
9
+ llm = create_llm().bind_tools(available_tools)
10
+
11
+
12
+
13
+
14
+
src/agent/tools.py ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from langchain_core.tools import tool
2
+ from typing import Annotated, Any, Dict, List
3
+ from images.image_generator import generate_image
4
+ from langgraph.prebuilt import InjectedState
5
+ import logging
6
+
7
+ logger = logging.getLogger(__name__)
8
+
9
+ def _err(msg: str) -> str:
10
+ logger.error(msg)
11
+ return f"{{ 'error': '{msg}' }}"
12
+
13
+ def _success(msg: str) -> str:
14
+ logger.info(msg)
15
+ return f"{{ 'success': '{msg}' }}"
16
+
17
+ @tool
18
+ async def generate_scene_image(
19
+ prompt: Annotated[
20
+ str,
21
+ "The prompt to generate an image from"
22
+ ],
23
+ state: InjectedState,
24
+ ) -> Annotated[
25
+ str,
26
+ "The path to the generated image"
27
+ ]:
28
+ """
29
+ Generate an image from a prompt and set current scene image.
30
+ """
31
+ try:
32
+ image_path, img_description = generate_image(prompt)
33
+ state["current_scene"]["image"] = image_path
34
+ state["current_scene"]["image_description"] = img_description
35
+ return _success(f"Image generated and set as current scene image: {img_description}")
36
+ except Exception as e:
37
+ return _err(str(e))
38
+
39
+
40
+ available_tools = [generate_scene_image]
src/audio/audio_generator.py ADDED
@@ -0,0 +1,103 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import asyncio
2
+ from google import genai
3
+ from google.genai import types
4
+ from config import settings
5
+ import os
6
+ import tempfile
7
+ import wave
8
+ import numpy as np
9
+ import queue
10
+ import logging
11
+ import gradio as gr
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+ client = genai.Client(api_key=settings.gemini_api_key.get_secret_value(), http_options={'api_version': 'v1alpha'})
16
+ audio_queue = queue.Queue(maxsize=1)
17
+
18
+ async def generate_music(request: gr.Request, receive_audio):
19
+ async with (
20
+ client.aio.live.music.connect(model='models/lyria-realtime-exp') as session,
21
+ asyncio.TaskGroup() as tg,
22
+ ):
23
+ # Set up task to receive server messages.
24
+ tg.create_task(receive_audio(session))
25
+
26
+ # Send initial prompts and config
27
+ await session.set_weighted_prompts(
28
+ prompts=[
29
+ types.WeightedPrompt(text='The mysterious music of the forest', weight=1.0),
30
+ ]
31
+ )
32
+ await session.set_music_generation_config(
33
+ config=types.LiveMusicGenerationConfig(bpm=90, temperature=1.0)
34
+ )
35
+ await session.play()
36
+ logger.info(f"Started music generation for session {request.session_hash}")
37
+ sessions[request.session_hash] = session
38
+
39
+ async def change_music_tone(request: gr.Request, new_tone):
40
+ logger.info(f"Changing music tone to {new_tone}")
41
+ session = sessions.get(request.session_hash)
42
+ if not session:
43
+ logger.error(f"No session found for request {request.session_hash}")
44
+ return
45
+ await session.reset_context()
46
+ await session.set_weighted_prompts(
47
+ prompts=[types.WeightedPrompt(text=new_tone, weight=1.0)]
48
+ )
49
+
50
+
51
+ SAMPLE_RATE = 48000
52
+
53
+ async def receive_audio(session):
54
+ """Process incoming audio from the music generation."""
55
+ while True:
56
+ try:
57
+ async for message in session.receive():
58
+ if message.server_content and message.server_content.audio_chunks:
59
+ audio_data = message.server_content.audio_chunks[0].data
60
+ audio_queue.put(audio_data)
61
+ await asyncio.sleep(10**-12)
62
+ except Exception as e:
63
+ logger.error(f"Error in receive_audio: {e}")
64
+ await asyncio.sleep(1)
65
+
66
+ sessions = {}
67
+
68
+ async def start_music_generation(request: gr.Request):
69
+ """Start the music generation in a separate thread."""
70
+ await generate_music(request, receive_audio)
71
+
72
+ async def cleanup_music_session(request: gr.Request):
73
+ if request.session_hash in sessions:
74
+ logger.info(f"Cleaning up music session for session {request.session_hash}")
75
+ await sessions[request.session_hash].stop()
76
+ del sessions[request.session_hash]
77
+
78
+ current_audio_file = None
79
+
80
+ def update_audio():
81
+ """Continuously stream audio from the queue."""
82
+ global current_audio_file
83
+ while True:
84
+ audio_data = audio_queue.get()
85
+ if isinstance(audio_data, bytes):
86
+ audio_array = np.frombuffer(audio_data, dtype=np.int16)
87
+ else:
88
+ audio_array = np.array(audio_data, dtype=np.int16)
89
+
90
+ temp_fd, temp_path = tempfile.mkstemp(suffix='.wav')
91
+ os.close(temp_fd)
92
+ # Write to WAV file
93
+ with wave.open(temp_path, 'wb') as wav_file:
94
+ wav_file.setnchannels(2) # Stereo
95
+ wav_file.setsampwidth(2) # 16-bit
96
+ wav_file.setframerate(SAMPLE_RATE)
97
+ wav_file.writeframes(audio_array.tobytes())
98
+
99
+ if current_audio_file:
100
+ os.remove(current_audio_file)
101
+
102
+ current_audio_file = temp_path
103
+ yield temp_path
src/config.py ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from dotenv import load_dotenv
2
+
3
+ load_dotenv()
4
+ from pydantic_settings import BaseSettings
5
+ import logging
6
+ from pydantic import SecretStr
7
+
8
+ logging.basicConfig(
9
+ level=logging.INFO,
10
+ format="%(levelname)s:\t%(asctime)s [%(name)s] %(message)s",
11
+ datefmt="%Y-%m-%d %H:%M:%S %z",
12
+ )
13
+
14
+ class BaseAppSettings(BaseSettings):
15
+ """Base settings class with common configuration."""
16
+
17
+ class Config:
18
+ env_file = ".env"
19
+ env_file_encoding = "utf-8"
20
+ extra = "ignore"
21
+
22
+ class AppSettings(BaseAppSettings):
23
+ gemini_api_key: SecretStr
24
+ top_p: float = 0.95
25
+ temperature: float = 0.5
26
+
27
+
28
+ settings = AppSettings()
src/css.py ADDED
@@ -0,0 +1,126 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Custom CSS for fullscreen image with overlay
2
+ custom_css = """
3
+ /* Make the image container fullscreen */
4
+ .image-container {
5
+ position: fixed !important;
6
+ top: 0 !important;
7
+ left: 0 !important;
8
+ width: 100vw !important;
9
+ height: 100vh !important;
10
+ z-index: 1 !important;
11
+ }
12
+
13
+ .image-container img {
14
+ width: 100vw !important;
15
+ height: 100vh !important;
16
+ object-fit: cover !important;
17
+ }
18
+
19
+ /* Style the overlay content */
20
+ .overlay-content {
21
+ position: fixed !important;
22
+ bottom: 0 !important;
23
+ left: 0 !important;
24
+ right: 0 !important;
25
+ background: linear-gradient(transparent, rgba(0,0,0,0.8)) !important;
26
+ padding: 40px 20px 20px !important;
27
+ z-index: 10 !important;
28
+ color: white !important;
29
+ }
30
+
31
+ /* Style the narrative text */
32
+ .narrative-text {
33
+ background: rgba(0,0,0,0.7) !important;
34
+ border: none !important;
35
+ color: white !important;
36
+ font-size: 18px !important;
37
+ line-height: 1.5 !important;
38
+ padding: 20px !important;
39
+ border-radius: 10px !important;
40
+ margin-bottom: 20px !important;
41
+ }
42
+
43
+ img {
44
+ pointer-events: none;
45
+ }
46
+
47
+ .narrative-text textarea {
48
+ background: transparent !important;
49
+ border: none !important;
50
+ color: white !important;
51
+ -webkit-text-fill-color: white !important;
52
+ font-size: 18px !important;
53
+ resize: none !important;
54
+ }
55
+
56
+ /* Style the choice buttons */
57
+ .choice-buttons {
58
+ background: rgba(0,0,0,0.7) !important;
59
+ border-radius: 10px !important;
60
+ padding: 15px !important;
61
+ }
62
+
63
+ .choice-buttons label {
64
+ color: white !important;
65
+ font-size: 16px !important;
66
+ margin-bottom: 10px !important;
67
+ }
68
+
69
+ /* Fix radio button backgrounds */
70
+ .choice-buttons input[type="radio"] {
71
+ background: transparent !important;
72
+ border: 2px solid white !important;
73
+ }
74
+
75
+ .choice-buttons input[type="radio"]:checked {
76
+ background: white !important;
77
+ }
78
+
79
+ .choice-buttons .form-radio {
80
+ background: transparent !important;
81
+ }
82
+
83
+ /* Style radio button containers */
84
+ .choice-buttons > div {
85
+ background: transparent !important;
86
+ }
87
+
88
+ .choice-buttons fieldset {
89
+ background: transparent !important;
90
+ border: none !important;
91
+ }
92
+
93
+ /* Remove any remaining white backgrounds */
94
+ .choice-buttons * {
95
+ background-color: transparent !important;
96
+ }
97
+
98
+ .choice-buttons input {
99
+ background-color: transparent !important;
100
+ border: 1px solid rgba(255,255,255,0.5) !important;
101
+ color: white !important;
102
+ }
103
+
104
+ .choice-buttons label span {
105
+ color: white !important;
106
+ }
107
+
108
+ /* Hide gradio header and footer */
109
+ .gradio-header, .gradio-footer {
110
+ display: none !important;
111
+ }
112
+
113
+ /* Hide image control buttons using correct DOM selector */
114
+ .image-container .icon-button-wrapper {
115
+ display: none !important;
116
+ }
117
+
118
+ .image-container .icon-buttons {
119
+ display: none !important;
120
+ }
121
+
122
+ /* Make form element transparent */
123
+ .overlay-content .form {
124
+ background: transparent !important;
125
+ }
126
+ """
src/game_constructor.py ADDED
@@ -0,0 +1,183 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ import json
3
+ import uuid
4
+ from game_setting import Character, GameSetting
5
+ from game_state import story, state, get_current_scene
6
+ from agent.llm_agent import process_user_input
7
+ from images.image_generator import generate_image
8
+
9
+ # Predefined suggestions for demo
10
+ SETTING_SUGGESTIONS = [
11
+ "A mystical forest shrouded in eternal twilight, where ancient trees whisper secrets and magical creatures roam freely",
12
+ "A sprawling cyberpunk metropolis in 2099, where neon lights illuminate towering skyscrapers and technology controls every aspect of life",
13
+ "A Victorian-era mansion on a remote cliff, filled with hidden passages, antique furniture, and an atmosphere of dark mysteries",
14
+ "A post-apocalyptic wasteland where survivors struggle to rebuild civilization among the ruins of the old world",
15
+ "A magical academy floating in the clouds, where young wizards learn to master their powers and uncover ancient spells",
16
+ ]
17
+
18
+ CHARACTER_SUGGESTIONS = [
19
+ {
20
+ "name": "Elena Nightwhisper",
21
+ "age": "25",
22
+ "background": "A skilled detective with supernatural intuition, haunted by visions of crimes before they happen",
23
+ "personality": "Determined, intuitive, struggles with self-doubt but fiercely protective of the innocent",
24
+ },
25
+ {
26
+ "name": "Marcus Steelborn",
27
+ "age": "32",
28
+ "background": "A former soldier turned cybernetic engineer in a dystopian future, seeking to expose corporate corruption",
29
+ "personality": "Brave, tech-savvy, has trust issues but deeply loyal to those who earn his respect",
30
+ },
31
+ {
32
+ "name": "Aria Moonstone",
33
+ "age": "19",
34
+ "background": "A young witch discovering her powers while attending a prestigious magical academy",
35
+ "personality": "Curious, ambitious, sometimes reckless but has a good heart and strong sense of justice",
36
+ },
37
+ {
38
+ "name": "Dr. Victoria Blackthorne",
39
+ "age": "45",
40
+ "background": "A renowned archaeologist who specializes in occult artifacts and ancient mysteries",
41
+ "personality": "Intelligent, sophisticated, perfectionist with a hidden romantic side",
42
+ },
43
+ ]
44
+
45
+ GENRE_OPTIONS = [
46
+ "Horror - Supernatural terror and psychological thrills",
47
+ "Detective/Mystery - Crime solving and investigation",
48
+ "Romance - Love stories and relationship drama",
49
+ "Fantasy - Magic and mythical creatures",
50
+ "Sci-Fi - Futuristic technology and space exploration",
51
+ "Adventure - Action-packed journeys and quests",
52
+ "Psychological Thriller - Mind games and suspense",
53
+ "Historical Fiction - Stories set in past eras",
54
+ ]
55
+
56
+
57
+ def load_setting_suggestion(suggestion: str):
58
+ """Load a predefined setting suggestion"""
59
+ return suggestion
60
+
61
+
62
+ def load_character_suggestion(character_name: str):
63
+ """Load a predefined character suggestion"""
64
+ if character_name == "None":
65
+ return "", "", "", ""
66
+
67
+ for char in CHARACTER_SUGGESTIONS:
68
+ if char["name"] in character_name:
69
+ return char["name"], char["age"], char["background"], char["personality"]
70
+ return "", "", "", ""
71
+
72
+
73
+ def save_game_config(
74
+ setting_desc: str,
75
+ char_name: str,
76
+ char_age: str,
77
+ char_background: str,
78
+ char_personality: str,
79
+ genre: str,
80
+ ):
81
+ """Save the game configuration to a JSON file"""
82
+ if not all(
83
+ [setting_desc, char_name, char_age, char_background, char_personality, genre]
84
+ ):
85
+ return "❌ Please fill in all fields before saving."
86
+
87
+ config = {
88
+ "id": str(uuid.uuid4()),
89
+ "setting": {"description": setting_desc},
90
+ "character": {
91
+ "name": char_name,
92
+ "age": char_age,
93
+ "background": char_background,
94
+ "personality": char_personality,
95
+ },
96
+ "genre": genre,
97
+ "created_at": str(uuid.uuid4()), # In real app, would use actual timestamp
98
+ }
99
+
100
+ try:
101
+ filename = f"game_config_{config['id'][:8]}.json"
102
+ with open(f"generated/{filename}", "w") as f:
103
+ json.dump(config, f, indent=2)
104
+ return f"✅ Game configuration saved as {filename}"
105
+ except Exception as e:
106
+ return f"❌ Error saving configuration: {str(e)}"
107
+
108
+ async def start_game_with_settings(
109
+ setting_desc: str,
110
+ char_name: str,
111
+ char_age: str,
112
+ char_background: str,
113
+ char_personality: str,
114
+ genre: str,
115
+ ):
116
+ """Initialize the game with custom settings and switch to game interface"""
117
+ if not all(
118
+ [setting_desc, char_name, char_age, char_background, char_personality, genre]
119
+ ):
120
+ return (
121
+ gr.update(visible=True), # constructor_interface
122
+ gr.update(visible=False), # game_interface
123
+ gr.update(
124
+ value="❌ Please fill in all fields before starting the game.",
125
+ visible=True,
126
+ ), # error_message
127
+ gr.update(),
128
+ gr.update(),
129
+ gr.update(), # game components unchanged
130
+ )
131
+
132
+ character = Character(
133
+ name=char_name,
134
+ age=char_age,
135
+ background=char_background,
136
+ personality=char_personality,
137
+ )
138
+
139
+ game_setting = GameSetting(character=character, setting=setting_desc, genre=genre)
140
+
141
+ # Initialize the game story with the custom settings
142
+ initial_story = f"""Welcome to your story, {game_setting.character.name}!
143
+
144
+ Setting: {game_setting.setting}
145
+
146
+ You are {game_setting.character.name}, a {game_setting.character.age}-year-old character. {game_setting.character.background}
147
+
148
+ Your personality: {game_setting.character.personality}
149
+
150
+ Genre: {game_setting.genre}
151
+
152
+ You find yourself at the beginning of your adventure. The world around you feels alive with possibilities. What do you choose to do first?
153
+
154
+ NOTE FOR THE ASSISTANT: YOU HAVE TO GENERATE THE IMAGE FOR THE START SCENE.
155
+ """
156
+
157
+ response = await process_user_input(initial_story)
158
+
159
+ img = "forest.jpg"
160
+ if response.change_scene.change_scene:
161
+ img_path, _ = await generate_image(response.change_scene.scene_description)
162
+ if img_path:
163
+ img = img_path
164
+
165
+ story["start"] = {
166
+ "text": response.game_message,
167
+ "image": img,
168
+ "choices": [option.option_description for option in response.player_options],
169
+ "music_tone": response.change_music.music_description,
170
+ }
171
+ state["scene"] = "start"
172
+
173
+ # Get the current scene data
174
+ scene_text, scene_image, scene_choices = get_current_scene()
175
+
176
+ return (
177
+ gr.update(visible=False), # constructor_interface
178
+ gr.update(visible=True), # game_interface
179
+ gr.update(visible=False), # error_message
180
+ gr.update(value=scene_text), # game_text
181
+ gr.update(value=scene_image), # game_image
182
+ gr.update(choices=scene_choices, value=None), # game_choices
183
+ )
src/game_setting.py ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pydantic import BaseModel
2
+
3
+ class Character(BaseModel):
4
+ name: str
5
+ age: str
6
+ background: str
7
+ personality: str
8
+
9
+ class GameSetting(BaseModel):
10
+ character: Character
11
+ setting: str
12
+ genre: str
src/game_state.py ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ story = {
3
+ "start": {
4
+ "text": "You wake up in a mysterious forest. What do you do?",
5
+ "image": "forest.jpg",
6
+ "choices": ["Explore", "Wait"],
7
+ "music_tone": "neutral",
8
+ },
9
+ }
10
+
11
+ state = {"scene": "start"}
12
+
13
+ def get_current_scene():
14
+ scene = story[state["scene"]]
15
+ return scene["text"], scene["image"], scene["choices"]
src/images/image_generator.py ADDED
@@ -0,0 +1,140 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from google import genai
2
+ from google.genai import types
3
+ import os
4
+ from PIL import Image
5
+ from io import BytesIO
6
+ from datetime import datetime
7
+ from config import settings
8
+ import logging
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+ client = genai.Client(api_key=settings.gemini_api_key.get_secret_value())
13
+
14
+ async def generate_image(prompt: str) -> tuple[str, str] | None:
15
+ """
16
+ Generate an image using Google's Gemini model and save it to generated/images directory.
17
+
18
+ Args:
19
+ prompt (str): The text prompt to generate the image from
20
+
21
+ Returns:
22
+ str: Path to the generated image file, or None if generation failed
23
+ """
24
+ # Ensure the generated/images directory exists
25
+ output_dir = "generated/images"
26
+ os.makedirs(output_dir, exist_ok=True)
27
+
28
+ try:
29
+ response = client.models.generate_content(
30
+ model="gemini-2.0-flash-preview-image-generation",
31
+ contents=prompt,
32
+ config=types.GenerateContentConfig(
33
+ response_modalities=['TEXT', 'IMAGE']
34
+ )
35
+ )
36
+
37
+ # Process the response parts
38
+ image_saved = False
39
+ for part in response.candidates[0].content.parts:
40
+ if part.text is not None:
41
+ logger.info(f"Generated text: {part.text}")
42
+ elif part.inline_data is not None:
43
+ # Create a filename with timestamp
44
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
45
+ filename = f"gemini_{timestamp}.png"
46
+ filepath = os.path.join(output_dir, filename)
47
+
48
+ # Save the image
49
+ image = Image.open(BytesIO(part.inline_data.data))
50
+ image.save(filepath, "PNG")
51
+ logger.info(f"Image saved to: {filepath}")
52
+ image_saved = True
53
+
54
+ return filepath, part.text
55
+
56
+ if not image_saved:
57
+ logger.error("No image was generated in the response.")
58
+ return None, None
59
+
60
+ except Exception as e:
61
+ logger.error(f"Error generating image: {e}")
62
+ return None, None
63
+
64
+
65
+ def modify_image(image_path: str, modification_prompt: str) -> str | None:
66
+ """
67
+ Modify an existing image using Google's Gemini model based on a text prompt.
68
+
69
+ Args:
70
+ image_path (str): Path to the existing image file
71
+ modification_prompt (str): The text prompt describing how to modify the image
72
+
73
+ Returns:
74
+ str: Path to the modified image file, or None if modification failed
75
+ """
76
+ # Ensure the generated/images directory exists
77
+ output_dir = "generated/images"
78
+ os.makedirs(output_dir, exist_ok=True)
79
+
80
+ # Check if the input image exists
81
+ if not os.path.exists(image_path):
82
+ logger.error(f"Error: Image file not found at {image_path}")
83
+ return None
84
+
85
+ key = settings.gemini_api_key.get_secret_value()
86
+
87
+ client = genai.Client(api_key=key)
88
+
89
+ try:
90
+ # Load the input image
91
+ input_image = Image.open(image_path)
92
+
93
+ # Make the API call with both text and image
94
+ response = client.models.generate_content(
95
+ model="gemini-2.0-flash-preview-image-generation",
96
+ contents=[modification_prompt, input_image],
97
+ config=types.GenerateContentConfig(
98
+ response_modalities=['TEXT', 'IMAGE']
99
+ )
100
+ )
101
+
102
+ # Process the response parts
103
+ image_saved = False
104
+ for part in response.candidates[0].content.parts:
105
+ if part.text is not None:
106
+ logger.info(f"Generated text: {part.text}")
107
+ elif part.inline_data is not None:
108
+ # Create a filename with timestamp
109
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
110
+ filename = f"gemini_modified_{timestamp}.png"
111
+ filepath = os.path.join(output_dir, filename)
112
+
113
+ # Save the modified image
114
+ modified_image = Image.open(BytesIO(part.inline_data.data))
115
+ modified_image.save(filepath, "PNG")
116
+ logger.info(f"Modified image saved to: {filepath}")
117
+ image_saved = True
118
+
119
+ return filepath, part.text
120
+
121
+ if not image_saved:
122
+ logger.error("No modified image was generated in the response.")
123
+ return None, None
124
+
125
+ except Exception as e:
126
+ logger.error(f"Error modifying image: {e}")
127
+ return None, None
128
+
129
+
130
+ if __name__ == "__main__":
131
+ # Example usage
132
+ sample_prompt = "A Luke Skywalker half height sprite with white background for visual novel game"
133
+ generated_image_path = generate_image(sample_prompt)
134
+
135
+ # if generated_image_path:
136
+ # # Example modification
137
+ # modification_prompt = "Now the house is destroyed, and the jawas are running away"
138
+ # modified_image_path = modify_image(generated_image_path, modification_prompt)
139
+ # if modified_image_path:
140
+ # print(f"Successfully modified image: {modified_image_path}")
src/main.py ADDED
@@ -0,0 +1,320 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ from css import custom_css
3
+ from audio.audio_generator import (
4
+ update_audio,
5
+ start_music_generation,
6
+ change_music_tone,
7
+ cleanup_music_session,
8
+ )
9
+ import logging
10
+ from agent.llm_agent import process_user_input
11
+ from images.image_generator import generate_image
12
+ import uuid
13
+ from game_state import story, state
14
+ from game_constructor import (
15
+ SETTING_SUGGESTIONS,
16
+ CHARACTER_SUGGESTIONS,
17
+ GENRE_OPTIONS,
18
+ load_setting_suggestion,
19
+ load_character_suggestion,
20
+ start_game_with_settings,
21
+ )
22
+ import asyncio
23
+
24
+ logger = logging.getLogger(__name__)
25
+
26
+
27
+ def return_to_constructor():
28
+ """Return to the game constructor interface"""
29
+ return (
30
+ gr.update(visible=True), # constructor_interface
31
+ gr.update(visible=False), # game_interface
32
+ gr.update(visible=False), # error_message
33
+ )
34
+
35
+
36
+ async def update_scene(request: gr.Request, choice):
37
+ logger.info(f"Updating scene with choice: {choice}")
38
+ if isinstance(choice, str):
39
+ old_scene = state["scene"]
40
+ new_scene = str(uuid.uuid4())
41
+ story[new_scene] = {
42
+ **story[old_scene],
43
+ }
44
+ state["scene"] = new_scene
45
+
46
+ user_story = f"""Current scene description:
47
+ {story[old_scene]["text"]}
48
+ User's choice: {choice}
49
+ """
50
+
51
+ response = await process_user_input(user_story)
52
+
53
+ story[new_scene]["text"] = response.game_message
54
+
55
+ story[new_scene]["choices"] = [
56
+ option.option_description for option in response.player_options
57
+ ]
58
+
59
+ if response.change_scene.change_scene:
60
+ img_path, _ = await generate_image(response.change_scene.scene_description)
61
+ if img_path:
62
+ story[new_scene]["image"] = img_path
63
+
64
+ if response.change_music.change_music:
65
+ await change_music_tone(request, response.change_music.music_description)
66
+
67
+ scene = story[state["scene"]]
68
+ return (
69
+ scene["text"],
70
+ scene["image"],
71
+ gr.Radio(
72
+ choices=scene["choices"],
73
+ label="What do you choose?",
74
+ value=None,
75
+ elem_classes=["choice-buttons"],
76
+ ),
77
+ )
78
+
79
+
80
+ def update_preview(setting, name, age, background, personality, genre):
81
+ """Update the configuration preview"""
82
+ if not any([setting, name, age, background, personality]):
83
+ return "Fill in the fields to see a preview..."
84
+
85
+ preview = f"""🌍 SETTING: {setting[:100]}{"..." if len(setting) > 100 else ""}
86
+
87
+ 👤 CHARACTER: {name} (Age: {age})
88
+ 📖 Background: {background}
89
+ 💭 Personality: {personality}
90
+
91
+ 🎭 GENRE: {genre}"""
92
+ return preview
93
+
94
+
95
+ async def start_game_with_music(
96
+ request: gr.Request,
97
+ setting_desc: str,
98
+ char_name: str,
99
+ char_age: str,
100
+ char_background: str,
101
+ char_personality: str,
102
+ genre: str,
103
+ ):
104
+ """Start the game with custom settings and initialize music"""
105
+ # First, get the game interface updates
106
+ result = await start_game_with_settings(
107
+ setting_desc, char_name, char_age, char_background, char_personality, genre
108
+ )
109
+ # If game is starting successfully (interface is switching), start music
110
+ if len(result) >= 2 and result[1].get(
111
+ "visible", False
112
+ ): # game_interface becoming visible
113
+ asyncio.create_task(start_music_generation(request))
114
+
115
+ if state["scene"] in story and "music_tone" in story[state["scene"]]:
116
+ await change_music_tone(request, story[state["scene"]]["music_tone"])
117
+
118
+ return result
119
+
120
+ with gr.Blocks(
121
+ theme="soft", title="Game Constructor & Visual Novel", css=custom_css
122
+ ) as demo:
123
+ # Constructor Interface (visible by default)
124
+ with gr.Column(visible=True, elem_id="constructor-interface") as constructor_interface:
125
+ gr.Markdown("# 🎮 Interactive Game Constructor")
126
+ gr.Markdown(
127
+ "Create your own interactive story game by defining the setting, character, and genre!"
128
+ )
129
+
130
+ # Error message area
131
+ error_message = gr.Textbox(
132
+ label="⚠️ Error",
133
+ visible=False,
134
+ interactive=False,
135
+ elem_classes=["error-message"],
136
+ )
137
+
138
+ with gr.Row():
139
+ with gr.Column(scale=2):
140
+ # Setting Description Section
141
+ with gr.Group():
142
+ gr.Markdown("## 🌍 Setting Description")
143
+ setting_suggestions = gr.Dropdown(
144
+ choices=["Select a suggestion..."] + SETTING_SUGGESTIONS,
145
+ label="Quick Suggestions",
146
+ value="Select a suggestion...",
147
+ interactive=True,
148
+ )
149
+ setting_description = gr.Textbox(
150
+ label="Describe your game setting",
151
+ placeholder="Enter a detailed description of where your story takes place...",
152
+ lines=4,
153
+ max_lines=6,
154
+ )
155
+
156
+ # Character Description Section
157
+ with gr.Group():
158
+ gr.Markdown("## 👤 Character Description")
159
+ character_suggestions = gr.Dropdown(
160
+ choices=["None"]
161
+ + [
162
+ f"{char['name']} - {char['background'][:50]}..."
163
+ for char in CHARACTER_SUGGESTIONS
164
+ ],
165
+ label="Character Templates",
166
+ value="None",
167
+ interactive=True,
168
+ )
169
+
170
+ with gr.Row():
171
+ char_name = gr.Textbox(
172
+ label="Character Name",
173
+ placeholder="Enter character name...",
174
+ )
175
+ char_age = gr.Textbox(label="Age", placeholder="25")
176
+
177
+ char_background = gr.Textbox(
178
+ label="Background/Profession",
179
+ placeholder="Describe your character's background, profession, or role...",
180
+ lines=2,
181
+ )
182
+ char_personality = gr.Textbox(
183
+ label="Personality & Traits",
184
+ placeholder="Describe personality, quirks, motivations, fears...",
185
+ lines=2,
186
+ )
187
+
188
+ # Genre Selection Section
189
+ with gr.Group():
190
+ gr.Markdown("## 🎭 Genre & Style")
191
+ genre_selection = gr.Dropdown(
192
+ choices=GENRE_OPTIONS,
193
+ label="Choose your story genre",
194
+ value=GENRE_OPTIONS[0],
195
+ interactive=True,
196
+ )
197
+
198
+ with gr.Column(scale=1):
199
+ # Preview Section
200
+ with gr.Group():
201
+ gr.Markdown("## 📋 Configuration Preview")
202
+ preview_box = gr.Textbox(
203
+ label="Game Summary",
204
+ lines=8,
205
+ interactive=False,
206
+ placeholder="Fill in the fields to see a preview...",
207
+ )
208
+
209
+ with gr.Group():
210
+ gr.Markdown("## 🎮 Ready to Play?")
211
+ start_btn = gr.Button("▶️ Start Game", variant="primary", size="lg")
212
+
213
+ with gr.Column(visible=False, elem_id="game-interface") as game_interface:
214
+ gr.Markdown("# 🎮 Your Interactive Story")
215
+
216
+ with gr.Row():
217
+ back_btn = gr.Button("⬅️ Back to Constructor", variant="secondary")
218
+ gr.Markdown("### Playing your custom game!")
219
+
220
+ # Audio component for background music
221
+ audio_out = gr.Audio(
222
+ autoplay=True, streaming=True, interactive=False, visible=False
223
+ )
224
+
225
+ # Background image (fullscreen)
226
+ with gr.Column(elem_classes=["image-container"]):
227
+ game_image = gr.Image(type="filepath", interactive=False, show_label=False)
228
+
229
+ # Overlay content (text and buttons)
230
+ with gr.Column(elem_classes=["overlay-content"]):
231
+ game_text = gr.Textbox(
232
+ label="",
233
+ interactive=False,
234
+ show_label=False,
235
+ elem_classes=["narrative-text"],
236
+ lines=3,
237
+ )
238
+ game_choices = gr.Radio(
239
+ choices=[],
240
+ label="What do you choose?",
241
+ value=None,
242
+ elem_classes=["choice-buttons"],
243
+ )
244
+
245
+ # Event handlers for constructor interface
246
+ setting_suggestions.change(
247
+ fn=load_setting_suggestion,
248
+ inputs=[setting_suggestions],
249
+ outputs=[setting_description],
250
+ )
251
+
252
+ character_suggestions.change(
253
+ fn=load_character_suggestion,
254
+ inputs=[character_suggestions],
255
+ outputs=[char_name, char_age, char_background, char_personality],
256
+ )
257
+
258
+ # Update preview when any field changes
259
+ for component in [
260
+ setting_description,
261
+ char_name,
262
+ char_age,
263
+ char_background,
264
+ char_personality,
265
+ genre_selection,
266
+ ]:
267
+ component.change(
268
+ fn=update_preview,
269
+ inputs=[
270
+ setting_description,
271
+ char_name,
272
+ char_age,
273
+ char_background,
274
+ char_personality,
275
+ genre_selection,
276
+ ],
277
+ outputs=[preview_box],
278
+ )
279
+
280
+ # Interface switching handlers
281
+ start_btn.click(
282
+ fn=start_game_with_music,
283
+ inputs=[
284
+ setting_description,
285
+ char_name,
286
+ char_age,
287
+ char_background,
288
+ char_personality,
289
+ genre_selection,
290
+ ],
291
+ outputs=[
292
+ constructor_interface,
293
+ game_interface,
294
+ error_message,
295
+ game_text,
296
+ game_image,
297
+ game_choices,
298
+ ],
299
+ )
300
+
301
+ back_btn.click(
302
+ fn=return_to_constructor,
303
+ inputs=[],
304
+ outputs=[constructor_interface, game_interface, error_message],
305
+ )
306
+
307
+ game_choices.change(
308
+ fn=update_scene,
309
+ inputs=[game_choices],
310
+ outputs=[game_text, game_image, game_choices],
311
+ )
312
+
313
+ demo.unload(cleanup_music_session)
314
+ demo.load(
315
+ fn=update_audio,
316
+ inputs=None,
317
+ outputs=[audio_out],
318
+ )
319
+
320
+ demo.launch()