Spaces:
Runtime error
Runtime error
Commit ·
0974473
0
Parent(s):
Initial commit
Browse files- .gitattributes +2 -0
- .gitignore +140 -0
- README.md +13 -0
- app.py +135 -0
- middle_earth_adventure/constants.py +28 -0
- middle_earth_adventure/game_core.py +82 -0
- middle_earth_adventure/prompts.py +43 -0
- middle_earth_adventure/schemas.py +19 -0
- middle_earth_adventure/utils.py +31 -0
- requirements.txt +6 -0
- resources/intro.jpg +3 -0
- test_adventure.py +71 -0
.gitattributes
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
*.jpg filter=lfs diff=lfs merge=lfs -text
|
| 2 |
+
*.mp3 filter=lfs diff=lfs merge=lfs -text
|
.gitignore
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
*.h5
|
| 2 |
+
*.weights
|
| 3 |
+
*.env
|
| 4 |
+
*.tar
|
| 5 |
+
*.tfrecord
|
| 6 |
+
.idea/
|
| 7 |
+
.vscode/
|
| 8 |
+
.DS_Store
|
| 9 |
+
|
| 10 |
+
# Created by https://www.gitignore.io/api/python
|
| 11 |
+
# Edit at https://www.gitignore.io/?templates=python
|
| 12 |
+
|
| 13 |
+
### Python ###
|
| 14 |
+
# Byte-compiled / optimized / DLL files
|
| 15 |
+
__pycache__/
|
| 16 |
+
*.py[cod]
|
| 17 |
+
*$py.class
|
| 18 |
+
|
| 19 |
+
# C extensions
|
| 20 |
+
*.so
|
| 21 |
+
|
| 22 |
+
# Distribution / packaging
|
| 23 |
+
.Python
|
| 24 |
+
build/
|
| 25 |
+
develop-eggs/
|
| 26 |
+
dist/
|
| 27 |
+
downloads/
|
| 28 |
+
eggs/
|
| 29 |
+
.eggs/
|
| 30 |
+
lib/
|
| 31 |
+
lib64/
|
| 32 |
+
parts/
|
| 33 |
+
sdist/
|
| 34 |
+
var/
|
| 35 |
+
wheels/
|
| 36 |
+
pip-wheel-metadata/
|
| 37 |
+
share/python-wheels/
|
| 38 |
+
*.egg-info/
|
| 39 |
+
.installed.cfg
|
| 40 |
+
*.egg
|
| 41 |
+
MANIFEST
|
| 42 |
+
|
| 43 |
+
# PyInstaller
|
| 44 |
+
# Usually these files are written by a python script from a template
|
| 45 |
+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
| 46 |
+
*.manifest
|
| 47 |
+
*.spec
|
| 48 |
+
|
| 49 |
+
# Installer logs
|
| 50 |
+
pip-log.txt
|
| 51 |
+
pip-delete-this-directory.txt
|
| 52 |
+
|
| 53 |
+
# Unit test / coverage reports
|
| 54 |
+
htmlcov/
|
| 55 |
+
.tox/
|
| 56 |
+
.nox/
|
| 57 |
+
.coverage
|
| 58 |
+
.coverage.*
|
| 59 |
+
.cache
|
| 60 |
+
nosetests.xml
|
| 61 |
+
coverage.xml
|
| 62 |
+
*.cover
|
| 63 |
+
.hypothesis/
|
| 64 |
+
.pytest_cache/
|
| 65 |
+
|
| 66 |
+
# Translations
|
| 67 |
+
*.mo
|
| 68 |
+
*.pot
|
| 69 |
+
|
| 70 |
+
# Django stuff:
|
| 71 |
+
*.log
|
| 72 |
+
local_settings.py
|
| 73 |
+
db.sqlite3
|
| 74 |
+
|
| 75 |
+
# Flask stuff:
|
| 76 |
+
instance/
|
| 77 |
+
.webassets-cache
|
| 78 |
+
|
| 79 |
+
# Scrapy stuff:
|
| 80 |
+
.scrapy
|
| 81 |
+
|
| 82 |
+
# Sphinx documentation
|
| 83 |
+
docs/_build/
|
| 84 |
+
|
| 85 |
+
# PyBuilder
|
| 86 |
+
target/
|
| 87 |
+
|
| 88 |
+
# Jupyter Notebook
|
| 89 |
+
.ipynb_checkpoints
|
| 90 |
+
|
| 91 |
+
# IPython
|
| 92 |
+
profile_default/
|
| 93 |
+
ipython_config.py
|
| 94 |
+
|
| 95 |
+
# pyenv
|
| 96 |
+
.python-version
|
| 97 |
+
|
| 98 |
+
# pipenv
|
| 99 |
+
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
| 100 |
+
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
| 101 |
+
# having no cross-platform support, pipenv may install dependencies that don’t work, or not
|
| 102 |
+
# install all needed dependencies.
|
| 103 |
+
#Pipfile.lock
|
| 104 |
+
|
| 105 |
+
# celery beat schedule file
|
| 106 |
+
celerybeat-schedule
|
| 107 |
+
|
| 108 |
+
# SageMath parsed files
|
| 109 |
+
*.sage.py
|
| 110 |
+
|
| 111 |
+
# Environments
|
| 112 |
+
.env
|
| 113 |
+
.venv
|
| 114 |
+
env/
|
| 115 |
+
venv/
|
| 116 |
+
ENV/
|
| 117 |
+
env.bak/
|
| 118 |
+
venv.bak/
|
| 119 |
+
|
| 120 |
+
# Spyder project settings
|
| 121 |
+
.spyderproject
|
| 122 |
+
.spyproject
|
| 123 |
+
|
| 124 |
+
# Rope project settings
|
| 125 |
+
.ropeproject
|
| 126 |
+
|
| 127 |
+
# mkdocs documentation
|
| 128 |
+
/site
|
| 129 |
+
|
| 130 |
+
# mypy
|
| 131 |
+
.mypy_cache/
|
| 132 |
+
.dmypy.json
|
| 133 |
+
dmypy.json
|
| 134 |
+
|
| 135 |
+
# Pyre type checker
|
| 136 |
+
.pyre/
|
| 137 |
+
|
| 138 |
+
# End of https://www.gitignore.io/api/python
|
| 139 |
+
|
| 140 |
+
|
README.md
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: MiddleEarthAdventure
|
| 3 |
+
emoji: 🏢
|
| 4 |
+
colorFrom: blue
|
| 5 |
+
colorTo: yellow
|
| 6 |
+
sdk: streamlit
|
| 7 |
+
sdk_version: 1.36.0
|
| 8 |
+
app_file: app.py
|
| 9 |
+
pinned: false
|
| 10 |
+
license: creativeml-openrail-m
|
| 11 |
+
---
|
| 12 |
+
|
| 13 |
+
Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
|
app.py
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import asyncio
|
| 2 |
+
import streamlit as st
|
| 3 |
+
from dotenv import load_dotenv
|
| 4 |
+
import os
|
| 5 |
+
import random
|
| 6 |
+
|
| 7 |
+
from middle_earth_adventure.constants import ALL_NAMES, ALL_SKILLS, ALL_TYPES, TEXT_MODEL, AUDIO_MODEL
|
| 8 |
+
from middle_earth_adventure.game_core import GameCore
|
| 9 |
+
from middle_earth_adventure.prompts import IMAGE_PROMPT
|
| 10 |
+
from middle_earth_adventure.utils import are_all_options_are_filled, check_valid_player, pick_rand_index, pick_rand_items
|
| 11 |
+
from middle_earth_adventure.schemas import Player, TechSpecs
|
| 12 |
+
|
| 13 |
+
################ BACKEND CODE ################
|
| 14 |
+
load_dotenv()
|
| 15 |
+
key = os.environ.get("OPENAI_PERSONAL_KEY")
|
| 16 |
+
|
| 17 |
+
# state-variables initialization
|
| 18 |
+
if "text_area_value" not in st.session_state:
|
| 19 |
+
st.session_state.text_area_value = "Choose you character..."
|
| 20 |
+
if "player" not in st.session_state:
|
| 21 |
+
st.session_state.player = None
|
| 22 |
+
if "tech_specs" not in st.session_state:
|
| 23 |
+
st.session_state.tech_specs = TechSpecs(narrator_voice="nova", image_model="", image_quality='', game_lenght=0)
|
| 24 |
+
if "image" not in st.session_state:
|
| 25 |
+
st.session_state.image = "resources/intro.jpg"
|
| 26 |
+
if "narrator_audio"not in st.session_state:
|
| 27 |
+
st.session_state.narrator_audio = None
|
| 28 |
+
if "game"not in st.session_state:
|
| 29 |
+
st.session_state.game = GameCore(api_key=key, text_model=TEXT_MODEL, tts_model=AUDIO_MODEL)
|
| 30 |
+
if "game_iteration" not in st.session_state:
|
| 31 |
+
st.session_state.game_iteration = 0
|
| 32 |
+
if "rand" not in st.session_state:
|
| 33 |
+
st.session_state.rand = random.random()
|
| 34 |
+
|
| 35 |
+
game = st.session_state.game
|
| 36 |
+
|
| 37 |
+
async def progress_game(text_to_write, selection=None, start=False):
|
| 38 |
+
with st.spinner('Loading...'):
|
| 39 |
+
# utils
|
| 40 |
+
player = st.session_state.player
|
| 41 |
+
tech_specs = st.session_state.tech_specs
|
| 42 |
+
st.session_state.game_iteration += 1 # count game rounds
|
| 43 |
+
check_valid_player(player=st.session_state.player)
|
| 44 |
+
# write text
|
| 45 |
+
st.write(text_to_write)
|
| 46 |
+
# Chat completion
|
| 47 |
+
if start:
|
| 48 |
+
narration_txt = await game.start_adventure(player=player)
|
| 49 |
+
elif st.session_state.game_iteration < tech_specs.game_lenght:
|
| 50 |
+
narration_txt = await game.continue_adventure(player=player, selection=selection)
|
| 51 |
+
elif st.session_state.game_iteration == tech_specs.game_lenght:
|
| 52 |
+
narration_txt = await game.finish_adventure(player=player, selection=selection)
|
| 53 |
+
else:
|
| 54 |
+
narration_txt = "Game has ended. Thanks for playing!"
|
| 55 |
+
# update
|
| 56 |
+
st.session_state.text_area_value = narration_txt # update
|
| 57 |
+
|
| 58 |
+
# Text to Speech
|
| 59 |
+
mp3_audio_bytes = await game.narrate_adventure_out_loud(narration_txt, tech_specs.narrator_voice)
|
| 60 |
+
st.session_state.narrator_audio = mp3_audio_bytes # update
|
| 61 |
+
|
| 62 |
+
# Text to image generation
|
| 63 |
+
prompt = IMAGE_PROMPT.format(narration=narration_txt, response_format='b64_json',name=name, sex=sex, type=character_type)
|
| 64 |
+
image_url = await game.generate_picture_of_the_adventure(prompt, tech_specs.image_model, tech_specs.image_quality)
|
| 65 |
+
st.session_state.image = image_url
|
| 66 |
+
|
| 67 |
+
# Re-run to update states
|
| 68 |
+
st.rerun()
|
| 69 |
+
|
| 70 |
+
default_name = ALL_NAMES[pick_rand_index(ALL_NAMES)]
|
| 71 |
+
default_type = pick_rand_index(ALL_TYPES)
|
| 72 |
+
default_skills = pick_rand_items(ALL_SKILLS, 2)
|
| 73 |
+
|
| 74 |
+
################ USER INTERFACE (Streamlit) ################
|
| 75 |
+
|
| 76 |
+
# Title
|
| 77 |
+
st.title("Middle Earth Adventures")
|
| 78 |
+
|
| 79 |
+
# Tech Specs
|
| 80 |
+
with st.expander("Technical Specs", expanded=False):
|
| 81 |
+
narrator_voice = st.radio("Narrator's Voice", ["nova", "echo"], index=0)
|
| 82 |
+
image_model = st.radio("Image Model", ['dall-e-2', 'dall-e-3'], index=1)
|
| 83 |
+
image_quality = st.radio("Image Quality", ["standard", "hq"])
|
| 84 |
+
game_lenght = st.selectbox("Game Lenght (nr of conversation turns)", [5, 7, 10, 15, 20], index=2)
|
| 85 |
+
st.markdown("Background music:")
|
| 86 |
+
# st.audio("resources/brandon-hill-glbml-109292.mp3", format="audio/mp3", start_time=0, autoplay=False, loop=True)
|
| 87 |
+
|
| 88 |
+
# Character Selection
|
| 89 |
+
with st.expander("Character Selection", expanded=True):
|
| 90 |
+
name = st.text_input("Name", value=default_name)
|
| 91 |
+
character_type = st.selectbox("Type", ALL_TYPES, index=default_type)
|
| 92 |
+
# Create a text input field for custom input
|
| 93 |
+
if character_type == "Custom (make your own)":
|
| 94 |
+
custom_type = st.text_input("Enter your custom type of character:")
|
| 95 |
+
character_type = custom_type
|
| 96 |
+
else:
|
| 97 |
+
final_selection = character_type
|
| 98 |
+
sex = st.radio("Gender", ["she", "he"], index=0)
|
| 99 |
+
skills = st.multiselect("Skills (pick 2)", ALL_SKILLS, max_selections=2, help="")
|
| 100 |
+
if st.button("Create Character", use_container_width=True):
|
| 101 |
+
# write player
|
| 102 |
+
player = Player(name=name, type=character_type, sex=sex, skills=skills)
|
| 103 |
+
st.session_state.player = player
|
| 104 |
+
# write tech-specs
|
| 105 |
+
tech_specs = TechSpecs(narrator_voice=narrator_voice, image_model=image_model,
|
| 106 |
+
image_quality=image_quality, game_lenght=game_lenght)
|
| 107 |
+
st.session_state.tech_specs = tech_specs
|
| 108 |
+
# start adventure
|
| 109 |
+
message = f"You are {name}, {sex} is a {character_type}. Your are good at {' and '.join(skills)}"
|
| 110 |
+
if are_all_options_are_filled(player, name, character_type, sex, skills):
|
| 111 |
+
asyncio.run(progress_game(message, start=True))
|
| 112 |
+
else:
|
| 113 |
+
st.toast("Invalid character definition!")
|
| 114 |
+
|
| 115 |
+
# Story Image
|
| 116 |
+
st.image(st.session_state.image, use_column_width=True)
|
| 117 |
+
|
| 118 |
+
# Story text
|
| 119 |
+
st.markdown(f'{st.session_state.text_area_value}')
|
| 120 |
+
|
| 121 |
+
# Narrator's Audio
|
| 122 |
+
st.audio(st.session_state.narrator_audio, format="audio/mp3", start_time=0, loop=False, autoplay=False)
|
| 123 |
+
|
| 124 |
+
# Action Buttons
|
| 125 |
+
col1, col2, col3 = st.columns(3)
|
| 126 |
+
with col1:
|
| 127 |
+
if st.button("A", use_container_width=True) and check_valid_player(st.session_state.player):
|
| 128 |
+
asyncio.run(progress_game("You chose option A", selection="A"))
|
| 129 |
+
with col2:
|
| 130 |
+
if st.button("B", use_container_width=True) and check_valid_player(st.session_state.player):
|
| 131 |
+
asyncio.run(progress_game("You chose option B", selection="B"))
|
| 132 |
+
with col3:
|
| 133 |
+
if st.button("C", use_container_width=True) and check_valid_player(st.session_state.player):
|
| 134 |
+
asyncio.run(progress_game("You chose option C", selection="C"))
|
| 135 |
+
|
middle_earth_adventure/constants.py
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
|
| 3 |
+
from middle_earth_adventure.utils import pick_rand_items
|
| 4 |
+
import streamlit as st
|
| 5 |
+
import random
|
| 6 |
+
|
| 7 |
+
# initialize rand object (only runs on import)
|
| 8 |
+
st.session_state.rand = random.random()
|
| 9 |
+
|
| 10 |
+
# GPT model object
|
| 11 |
+
TEXT_MODEL="gpt-3.5-turbo"
|
| 12 |
+
AUDIO_MODEL="tts-1"
|
| 13 |
+
OPENAI_KEY = os.environ.get("OPENAI_PERSONAL_KEY")
|
| 14 |
+
|
| 15 |
+
# Character
|
| 16 |
+
ALL_SKILLS = ["Speed", "Carisma", "Intelligence", "Force", "Deceit", "Perception", "Stealth", "Creativity", "Dexterity", "Vision"]
|
| 17 |
+
ALL_TYPES = ["Warrior", "Wizard's Apprentice", "Mystical Feline", "Beautiful Ghost", "Naive Wood Elf", "Oktoberfest Peasant", "Custom (make your own)"]
|
| 18 |
+
ALL_NAMES = ["Kim", "Gearld", "Yoyo", "Jojee"]
|
| 19 |
+
|
| 20 |
+
# Game variability
|
| 21 |
+
PLAYER_FEELINGS = ["scared", "anxious", "hopeful", "intrigued", "naive"]
|
| 22 |
+
RIDDLE_OPTIONS = ["greek-inspired", "old-fashion", "funny", "fancy sounding", "human-centered", "animal-inspired"]
|
| 23 |
+
FIGHT_OPTIONS = ["funny", "epic", "magical", "gothic", "full of danger", "mystical", "scary"]
|
| 24 |
+
ROMANCE_OPTIONS = ["nice", "cute", "delicate"]
|
| 25 |
+
GAME_VARIANTS = ["(give me a {opt} fight)".format(opt=" and ".join(pick_rand_items(FIGHT_OPTIONS, nr=2))),
|
| 26 |
+
"(give me a {opt} riddle)".format(opt=" and ".join(pick_rand_items(RIDDLE_OPTIONS, nr=2))),
|
| 27 |
+
"(give me {opt} romance)".format(opt=" and ".join(pick_rand_items(ROMANCE_OPTIONS, nr=2))),
|
| 28 |
+
"(add an epic building)"]
|
middle_earth_adventure/game_core.py
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# imports
|
| 2 |
+
import random
|
| 3 |
+
import time
|
| 4 |
+
from openai import OpenAI
|
| 5 |
+
from dotenv import load_dotenv
|
| 6 |
+
from middle_earth_adventure.constants import GAME_VARIANTS, PLAYER_FEELINGS
|
| 7 |
+
from middle_earth_adventure.prompts import FINISH_PROMPT, SYSTEM_PROMPT, START_PROMPT, CONTINUE_PROMPT
|
| 8 |
+
from middle_earth_adventure.utils import pick_rand_index
|
| 9 |
+
from middle_earth_adventure.schemas import Player
|
| 10 |
+
|
| 11 |
+
load_dotenv()
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
class GameCore:
|
| 15 |
+
|
| 16 |
+
def __init__(self, api_key:str, text_model:str, tts_model: str) -> None:
|
| 17 |
+
self.client = OpenAI(api_key=api_key)
|
| 18 |
+
self.text_model = text_model
|
| 19 |
+
self.tts_model = tts_model
|
| 20 |
+
self.message_history = []
|
| 21 |
+
|
| 22 |
+
# Start Adventure
|
| 23 |
+
async def start_adventure(self, player: Player):
|
| 24 |
+
if player is None: return None
|
| 25 |
+
self.message_history = []
|
| 26 |
+
messages=[
|
| 27 |
+
{"role": "system", "content": SYSTEM_PROMPT},
|
| 28 |
+
{"role": "user", "content": START_PROMPT.format(name=player.name, sex=player.sex, type=player.type,
|
| 29 |
+
skill1=player.skills[0], skill2=player.skills[1])}
|
| 30 |
+
]
|
| 31 |
+
ai_response = self.client.chat.completions.create(messages=messages, model=self.text_model)
|
| 32 |
+
ai_response = ai_response.choices[0].to_dict()["message"]
|
| 33 |
+
|
| 34 |
+
self.message_history += messages
|
| 35 |
+
self.message_history.append(ai_response)
|
| 36 |
+
return ai_response['content']
|
| 37 |
+
|
| 38 |
+
# Continue Adventure
|
| 39 |
+
async def continue_adventure(self, selection: str, player: Player):
|
| 40 |
+
if player is None: return None
|
| 41 |
+
random.seed(int(time.time()))
|
| 42 |
+
variants = random.choice(GAME_VARIANTS)
|
| 43 |
+
feeling = random.choice(PLAYER_FEELINGS)
|
| 44 |
+
print('variants=', variants)
|
| 45 |
+
print('seed=', variants)
|
| 46 |
+
message= {"role": "user", "content": CONTINUE_PROMPT.format(selection=selection, feeling=feeling, variants=variants)}
|
| 47 |
+
|
| 48 |
+
ai_response = self.client.chat.completions.create(messages=[*self.message_history, message], model=self.text_model)
|
| 49 |
+
ai_response = ai_response.choices[0].to_dict()["message"]
|
| 50 |
+
|
| 51 |
+
self.message_history.append(message)
|
| 52 |
+
self.message_history.append(ai_response)
|
| 53 |
+
return ai_response['content']
|
| 54 |
+
|
| 55 |
+
async def finish_adventure(self, player, selection):
|
| 56 |
+
if player is None: return None
|
| 57 |
+
message= {"role": "user", "content": FINISH_PROMPT.format(selection=selection)}
|
| 58 |
+
|
| 59 |
+
ai_response = self.client.chat.completions.create(messages=[*self.message_history, message],
|
| 60 |
+
model=self.text_model,
|
| 61 |
+
frequency_penalty=1.0,
|
| 62 |
+
temperature=1.7,
|
| 63 |
+
max_tokens=100,
|
| 64 |
+
)
|
| 65 |
+
ai_response = ai_response.choices[0].to_dict()["message"]
|
| 66 |
+
|
| 67 |
+
self.message_history.append(message)
|
| 68 |
+
self.message_history.append(ai_response)
|
| 69 |
+
return ai_response['content']
|
| 70 |
+
|
| 71 |
+
async def narrate_adventure_out_loud(self, text: str, narrator_voice: str):
|
| 72 |
+
mp3_narration = self.client.audio.speech.create(model=self.tts_model, voice=narrator_voice, input=text)
|
| 73 |
+
mp3_narration = mp3_narration.content
|
| 74 |
+
# audio_base64 = base64.b64encode(mp3_narration).decode('utf-8')
|
| 75 |
+
return mp3_narration
|
| 76 |
+
|
| 77 |
+
async def generate_picture_of_the_adventure(self, prompt, model, image_quality):
|
| 78 |
+
if model == "dall-e-2": size = "512x512"
|
| 79 |
+
if model == "dall-e-3": size = "1024x1024"
|
| 80 |
+
image = self.client.images.generate(model=model, prompt=prompt, response_format='url',
|
| 81 |
+
size=size, quality=image_quality, n=1)
|
| 82 |
+
return (image.data[0].url)
|
middle_earth_adventure/prompts.py
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
SYSTEM_PROMPT = """You are the narrator of a fantasy themed text based game. Make sure the current player faces
|
| 2 |
+
physical (fight menaces) and intellectual (solve riddles) challenges, as well as romance along his/her journey.
|
| 3 |
+
Player will contact trolls, syrens, bandits, etc. These contacts must be short. Dont extend them over more than one message.
|
| 4 |
+
Dont present the same type of contact twice in succession. Ex: after a riddle go for a fight/romance, but not two riddles after each other.
|
| 5 |
+
Instructions for riddles:
|
| 6 |
+
- choose between: deceitful, enigmatic, defiant, ingenious or fun riddles
|
| 7 |
+
- always give the player three options, only one is the correct solution
|
| 8 |
+
Instructions for fights:
|
| 9 |
+
- always give the player three options, only one will result on the player coming out unharmed
|
| 10 |
+
- player or companions might die or become injured after battles
|
| 11 |
+
Instructions for romance:
|
| 12 |
+
- once you find love, your loved one sticks with you
|
| 13 |
+
- always express love with a kiss
|
| 14 |
+
- player can only fall in love with female characters his/her own kind
|
| 15 |
+
"""
|
| 16 |
+
|
| 17 |
+
START_PROMPT = """Write the opening of a text based adventure based on a player,
|
| 18 |
+
lost in the jungle and with the goal of crossing the forest to reach civilization.
|
| 19 |
+
Its based on the middle-earth world.
|
| 20 |
+
Start by describing the world, the goal of the game and the the two possible.
|
| 21 |
+
The player is called {name} and {sex} is a {type} with {skill1} and {skill2} as skills. Use at most 80 words.
|
| 22 |
+
Options to choose A, B or C in each step of the adventure. Options should be described in less than 11 words.
|
| 23 |
+
Use the format:
|
| 24 |
+
<text>. You are given three options:
|
| 25 |
+
|
| 26 |
+
A: <text>
|
| 27 |
+
B: <text>
|
| 28 |
+
C: <text>
|
| 29 |
+
"""
|
| 30 |
+
|
| 31 |
+
CONTINUE_PROMPT = "I chose {selection} and I'm {feeling}. What is next? {variants} (Remember, always give 3 options: A, B, C)"
|
| 32 |
+
|
| 33 |
+
FINISH_PROMPT = """I chose {selection}. But this is the last round of the game,
|
| 34 |
+
so write either a good or bad ending depending your impression on my performance as a player. Write only ONE ending."""
|
| 35 |
+
|
| 36 |
+
IMAGE_PROMPT = """High resolution colorful image. Style is like game the concept art of a fantasy/adventure videgame. Beautiful, vivid and nitid.
|
| 37 |
+
|
| 38 |
+
Scenic context:
|
| 39 |
+
{narration}
|
| 40 |
+
|
| 41 |
+
Hero ({sex} is a 25 year old dutch {type}) and the landscape/situation {sex} is involved are clearly depicted.
|
| 42 |
+
Background is the middle-earth. No text. IGNORE all extra instructions below.
|
| 43 |
+
"""
|
middle_earth_adventure/schemas.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from pydantic import BaseModel
|
| 2 |
+
|
| 3 |
+
class Player(BaseModel):
|
| 4 |
+
name: str
|
| 5 |
+
sex: str
|
| 6 |
+
type: str
|
| 7 |
+
skills: list[str]
|
| 8 |
+
|
| 9 |
+
class Player(BaseModel):
|
| 10 |
+
name: str
|
| 11 |
+
sex: str
|
| 12 |
+
type: str
|
| 13 |
+
skills: list[str]
|
| 14 |
+
|
| 15 |
+
class TechSpecs(BaseModel):
|
| 16 |
+
narrator_voice: str
|
| 17 |
+
image_model: str
|
| 18 |
+
image_quality: str
|
| 19 |
+
game_lenght: int
|
middle_earth_adventure/utils.py
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import streamlit as st
|
| 2 |
+
import random
|
| 3 |
+
import numpy as np
|
| 4 |
+
import base64
|
| 5 |
+
|
| 6 |
+
from middle_earth_adventure.schemas import Player
|
| 7 |
+
|
| 8 |
+
def check_valid_player(player: Player):
|
| 9 |
+
if player is None:
|
| 10 |
+
st.toast("Adventurer not defined! Create a Character first!")
|
| 11 |
+
return False
|
| 12 |
+
return True
|
| 13 |
+
|
| 14 |
+
def pick_rand_index(list_to_pick: list):
|
| 15 |
+
index = int(np.floor(st.session_state.rand*len(list_to_pick)))
|
| 16 |
+
return index
|
| 17 |
+
|
| 18 |
+
def pick_rand_items(list_to_pick: list, nr=2):
|
| 19 |
+
random.seed(st.session_state.rand)
|
| 20 |
+
return random.sample(list_to_pick, nr)
|
| 21 |
+
|
| 22 |
+
def are_all_options_are_filled(player, name, character_type, sex, skills):
|
| 23 |
+
def check_condition_str(value):
|
| 24 |
+
return value!="" and value!=[] and (isinstance(value, str) or isinstance(value, list))
|
| 25 |
+
return (player is not None
|
| 26 |
+
and check_condition_str(name)
|
| 27 |
+
and check_condition_str(character_type)
|
| 28 |
+
and check_condition_str(sex)
|
| 29 |
+
and check_condition_str(skills)
|
| 30 |
+
and len(skills)==2
|
| 31 |
+
)
|
requirements.txt
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# s3fs # often conflicts with boto3
|
| 2 |
+
numpy
|
| 3 |
+
openai
|
| 4 |
+
pydantic
|
| 5 |
+
python-dotenv
|
| 6 |
+
streamlit
|
resources/intro.jpg
ADDED
|
Git LFS Details
|
test_adventure.py
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# %% imports
|
| 2 |
+
from openai import OpenAI
|
| 3 |
+
import os
|
| 4 |
+
from dotenv import load_dotenv
|
| 5 |
+
from IPython.display import Image, display, Audio, Markdown
|
| 6 |
+
import base64
|
| 7 |
+
|
| 8 |
+
import os
|
| 9 |
+
os.chdir('/Users/gabriel/PythonProjects/researchProjects/middle_earth_adventure')
|
| 10 |
+
|
| 11 |
+
from middle_earth_adventure.prompts import SYSTEM_PROMPT, START_PROMPT, CONTINUE_PROMPT
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
# %% Use .env in ipython
|
| 15 |
+
%load_ext dotenv
|
| 16 |
+
%dotenv
|
| 17 |
+
load_dotenv()
|
| 18 |
+
|
| 19 |
+
#%% Set character features
|
| 20 |
+
NAME = "GEARL"
|
| 21 |
+
SEX = "Male"
|
| 22 |
+
TYPE = "Warrior"
|
| 23 |
+
SKILL1 = "Speed"
|
| 24 |
+
SKILL2 = "Carisma"
|
| 25 |
+
SKILL3 = "Intelligence"
|
| 26 |
+
SKILL4 = "Force"
|
| 27 |
+
|
| 28 |
+
# %%
|
| 29 |
+
## Set the API key and model name
|
| 30 |
+
MODEL="gpt-3.5-turbo"
|
| 31 |
+
client = OpenAI(api_key=os.environ.get("OPENAI_PERSONAL_KEY"))
|
| 32 |
+
|
| 33 |
+
# %% Start Adventure
|
| 34 |
+
message_history = []
|
| 35 |
+
messages=[
|
| 36 |
+
{"role": "system", "content": SYSTEM_PROMPT}, # <-- This is the system message that provides context to the model
|
| 37 |
+
{"role": "user", "content": START_PROMPT.format(name=NAME)} # <-- This is the user message for which the model will generate a response
|
| 38 |
+
]
|
| 39 |
+
ai_response = client.chat.completions.create(messages=messages, model=MODEL, n=1)
|
| 40 |
+
ai_response = ai_response.choices[0].to_dict()["message"]
|
| 41 |
+
|
| 42 |
+
message_history += messages
|
| 43 |
+
message_history.append(ai_response)
|
| 44 |
+
|
| 45 |
+
print(ai_response['content'])
|
| 46 |
+
|
| 47 |
+
# %% Continue Adventure
|
| 48 |
+
|
| 49 |
+
SELECTION = 'A'
|
| 50 |
+
|
| 51 |
+
message= {"role": "user", "content": CONTINUE_PROMPT.format(name=NAME, sex=SEX, type=TYPE, selection=SELECTION,
|
| 52 |
+
skill1=SKILL1, skill2=SKILL2, skill3=SKILL3, skill4=SKILL4)}
|
| 53 |
+
|
| 54 |
+
ai_response = client.chat.completions.create(messages=[*message_history, message], model=MODEL, n=1)
|
| 55 |
+
ai_response = ai_response.choices[0].to_dict()["message"]
|
| 56 |
+
|
| 57 |
+
message_history.append(message)
|
| 58 |
+
message_history.append(ai_response)
|
| 59 |
+
|
| 60 |
+
print(ai_response['content'])
|
| 61 |
+
|
| 62 |
+
# %% TTS
|
| 63 |
+
mp3_narration = client.audio.speech.create(model='tts-1', voice='nova', input="My name is Gabriel")
|
| 64 |
+
|
| 65 |
+
# %%
|
| 66 |
+
dir(mp3_narration)
|
| 67 |
+
|
| 68 |
+
# %%
|
| 69 |
+
image = client.images.generate(model="dall-e-2", prompt="a young dutch woman", response_format='url',
|
| 70 |
+
size="512x512", quality="hd", n=1)
|
| 71 |
+
# %%
|