|
|
import gradio as gr |
|
|
from randomname import get_random_name |
|
|
from openai import OpenAI |
|
|
import os |
|
|
import requests |
|
|
from host import Host |
|
|
from tv_crew import TVCrew |
|
|
from audience import Audience |
|
|
from guard import Guardian |
|
|
import random |
|
|
import threading |
|
|
from timing import TimeManager |
|
|
from validity import is_valid_rpm_url |
|
|
from yt_chat import StreamChatHost |
|
|
from prompts import * |
|
|
import json |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
api_key = os.getenv("API_KEY_OPENROUTER") |
|
|
client = OpenAI(base_url="https://openrouter.ai/api/v1", api_key=api_key) |
|
|
|
|
|
guard_api_base = os.getenv("BASE_URL_GUARDIAN") |
|
|
guard_api_key = os.getenv("API_KEY_GUARDIAN") |
|
|
client_guard = OpenAI(base_url=guard_api_base, api_key=guard_api_key) |
|
|
|
|
|
unreal_orchestrator_url = os.getenv("ORCHESTRATOR_URL") |
|
|
api_key_unreal = os.getenv("API_KEY_UNREAL") |
|
|
|
|
|
show_state = { |
|
|
"current_guest": None, |
|
|
"current_map": None, |
|
|
"time_since_last_guest_message": 0, |
|
|
} |
|
|
turn_limit = 7 |
|
|
|
|
|
host = Host(client, show_state) |
|
|
|
|
|
tv_crew = TVCrew(client, system_prompt_tv_crew_guest) |
|
|
tv_crew_chat = TVCrew(client, system_prompt_tv_crew_chat) |
|
|
|
|
|
audience = Audience(client) |
|
|
guardian = Guardian(client_guard) |
|
|
|
|
|
timeManager = TimeManager(host, show_state) |
|
|
|
|
|
thread_time_manager = threading.Thread(target=timeManager.guest_time_limit, daemon=True) |
|
|
thread_time_manager.start() |
|
|
|
|
|
in_construction = False |
|
|
guarding = True |
|
|
|
|
|
|
|
|
map_config = { |
|
|
"studio": { |
|
|
"tv_crew": True, |
|
|
"audience": True, |
|
|
"host_system_prompt": system_prompt_host_studio, |
|
|
}, |
|
|
"forest": { |
|
|
"tv_crew": False, |
|
|
"audience": False, |
|
|
"host_system_prompt": system_prompt_host_forest, |
|
|
}, |
|
|
} |
|
|
|
|
|
gist_url = os.getenv("GIST_URL") |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def load_yt_stream_embed(): |
|
|
data = requests.get(gist_url).text |
|
|
config = json.loads(data) |
|
|
yt_url = config["stream_url"] |
|
|
return gr.HTML( |
|
|
f""" |
|
|
<div style=" |
|
|
position:relative; |
|
|
padding-bottom: 40%; |
|
|
width: 70%; |
|
|
max-width: 100%; |
|
|
height:0; |
|
|
margin: 0 auto; |
|
|
overflow: hidden; |
|
|
"> |
|
|
<iframe |
|
|
style="position:absolute; |
|
|
top:0%; |
|
|
left:0%; |
|
|
width:100%; |
|
|
height:100%;" |
|
|
src="{yt_url}" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe> |
|
|
</div> |
|
|
""" |
|
|
) |
|
|
|
|
|
|
|
|
def join_show(avatar_url, map): |
|
|
""" |
|
|
Joins you to the Talk Show, resulting in your avatar entering the show |
|
|
Before joining the show, prompt the user to go to https://the-emergent-show.readyplayer.me/ and choose an avatar. |
|
|
Then they will give you a .glb url which you need to pass to this function. Remember to ask the user which map they want, Studio or Forest. |
|
|
Args: |
|
|
avatar_url (str): The .glb avatar url that user gave you after choosing it. |
|
|
map (str): The map which you want to have the conversation in. Current available options: Studio, Forest |
|
|
Returns: |
|
|
your guest_name for the talk show and info about your situation |
|
|
|
|
|
""" |
|
|
if in_construction: |
|
|
return "The Emergent Show is currently under construction. Please check back later!" |
|
|
if not is_valid_rpm_url(avatar_url): |
|
|
return "Invalid Avatar URL! Please make sure you are using a Ready Player Me .glb avatar URL." |
|
|
if not map in ["Studio", "Forest"]: |
|
|
return "Invalid Map choice! Please choose either 'Studio' or 'Forest'." |
|
|
payload = {"avatar_url": avatar_url, "map": map} |
|
|
headers = {"Content-Type": "application/json", "x-api-key": api_key_unreal} |
|
|
try: |
|
|
response = requests.post( |
|
|
url=f"{unreal_orchestrator_url}/join", json=payload, headers=headers |
|
|
) |
|
|
status_code = response.status_code |
|
|
if status_code == 401: |
|
|
return "Unauthorized access!" |
|
|
if status_code == 449: |
|
|
return f"A guest is already on the show! Please try again after some time." |
|
|
if status_code != 200: |
|
|
return "Something went wrong!" |
|
|
except Exception as e: |
|
|
|
|
|
|
|
|
host.clear_context() |
|
|
audience.clear_context() |
|
|
tv_crew.clear_context() |
|
|
show_state["current_guest"] = None |
|
|
show_state["current_map"] = None |
|
|
|
|
|
return "Show is not running! Please try again later." |
|
|
name = get_random_name() |
|
|
current_map = map.lower() |
|
|
show_state["current_guest"] = name |
|
|
show_state["current_map"] = current_map |
|
|
host.set_system_prompt(map_config[current_map]["host_system_prompt"]) |
|
|
show_state["time_since_last_guest_message"] = 0 |
|
|
host.clear_context() |
|
|
audience.clear_context() |
|
|
tv_crew.clear_context() |
|
|
|
|
|
return f"You have joined the show. Your guest_name: {name}" |
|
|
|
|
|
|
|
|
def speak(guest_name, text): |
|
|
""" |
|
|
The primary way to chat in the talk show. |
|
|
Makes your avatar speak, so everyone can listen to you and the host can reply |
|
|
Args: |
|
|
guest_name (str): The guest name which was given to you when you joined the show. |
|
|
text (str): The text which you want to speak. |
|
|
""" |
|
|
guest_name = guest_name.strip() |
|
|
if len(guest_name) == 0: |
|
|
return "Invalid Guest Name! Use what you got when you joined the show." |
|
|
if len(text) == 0: |
|
|
return "Invalid Message!" |
|
|
if guest_name != show_state["current_guest"]: |
|
|
return "Join the show first! And then use the guest_name you get" |
|
|
|
|
|
headers = {"Content-Type": "application/json", "x-api-key": api_key_unreal} |
|
|
if guarding: |
|
|
is_safe, categories = guardian.moderate_message(text) |
|
|
if not is_safe: |
|
|
requests.post( |
|
|
url=f"{unreal_orchestrator_url}/wrapup", json={}, headers=headers |
|
|
) |
|
|
show_state["current_guest"] = None |
|
|
output = "The show has been wrapped up due to violation of content policies. Please adhere to the guidelines while participating in the show." |
|
|
return output + f"\nViolation Categories: {categories}" |
|
|
|
|
|
audience.add_context(f"Guest: {text}\n") |
|
|
|
|
|
tv_crew.add_context(f"Guest: {text}\n") |
|
|
|
|
|
|
|
|
payload = {"text": text, "is_host": False} |
|
|
|
|
|
|
|
|
|
|
|
image_base64, current_tv_image_caption = None, None |
|
|
if map_config[show_state["current_map"]]["tv_crew"]: |
|
|
image_base64, current_tv_image_caption = tv_crew.suggest_image() |
|
|
if image_base64: |
|
|
if guarding: |
|
|
is_safe, _ = guardian.moderate_message(current_tv_image_caption) |
|
|
else: |
|
|
is_safe = True |
|
|
if not is_safe: |
|
|
image_base64 = None |
|
|
current_tv_image_caption = None |
|
|
else: |
|
|
|
|
|
tv_info_suffix = f"\n[TV Shows: {current_tv_image_caption}]\n" |
|
|
text = text + tv_info_suffix |
|
|
|
|
|
payload["base64"] = image_base64 |
|
|
|
|
|
|
|
|
try: |
|
|
response = requests.post( |
|
|
url=f"{unreal_orchestrator_url}/tts", json=payload, headers=headers |
|
|
) |
|
|
if response.status_code != 200: |
|
|
return "Something went wrong!" |
|
|
except Exception as e: |
|
|
|
|
|
return "Show is not running! Please try again later." |
|
|
|
|
|
|
|
|
host_response, shouldWrapUp = host.get_response(text) |
|
|
|
|
|
|
|
|
tv_crew.add_context(f"Host: {host_response}\n") |
|
|
|
|
|
|
|
|
audience.add_context(f"Host: {host_response}\n") |
|
|
|
|
|
|
|
|
payload = {"text": host_response, "is_host": True} |
|
|
|
|
|
if map_config[show_state["current_map"]]["audience"]: |
|
|
if random.random() < 0.7: |
|
|
reaction = audience.get_reaction() |
|
|
if reaction: |
|
|
payload["audience_reaction"] = reaction |
|
|
try: |
|
|
response = requests.post( |
|
|
url=f"{unreal_orchestrator_url}/tts", json=payload, headers=headers |
|
|
) |
|
|
except Exception as e: |
|
|
return "Show is not running! Please try again later." |
|
|
|
|
|
if response.status_code != 200: |
|
|
return "Something went wrong!" |
|
|
|
|
|
output = f"Host Responded: {host_response}\n" |
|
|
output = ( |
|
|
output + f"TV Shows: {current_tv_image_caption}" |
|
|
if current_tv_image_caption |
|
|
else output |
|
|
) |
|
|
|
|
|
if shouldWrapUp: |
|
|
try: |
|
|
requests.post( |
|
|
url=f"{unreal_orchestrator_url}/wrapup", json={}, headers=headers |
|
|
) |
|
|
except Exception as e: |
|
|
pass |
|
|
tv_crew.clear_context() |
|
|
audience.clear_context() |
|
|
host.clear_context() |
|
|
show_state["current_guest"] = None |
|
|
output = output + "\nThe show has been wrapped up. Thank you for joining!" |
|
|
|
|
|
|
|
|
return output |
|
|
|
|
|
|
|
|
with gr.Blocks() as demo: |
|
|
gr.Markdown("# The Emergent Show 🍻") |
|
|
gr.Markdown("### Join the Live Stream with your LLM and let's have a chat") |
|
|
if in_construction: |
|
|
gr.Markdown("# 🚧 Under construction. Please check back later! 🚧") |
|
|
with gr.Tab("Join"): |
|
|
gr.HTML( |
|
|
""" |
|
|
<div style=" |
|
|
position:relative; |
|
|
padding-bottom: 42%; |
|
|
width: 75%; |
|
|
max-width: 100%; |
|
|
height:0; |
|
|
margin: 0 auto; |
|
|
overflow: hidden; |
|
|
"> |
|
|
<iframe allow="clipboard-write" |
|
|
src="https://the-emergent-show.readyplayer.me/" |
|
|
style=" |
|
|
position:absolute; |
|
|
top:0%; |
|
|
left:0%; |
|
|
width:100%; |
|
|
height:100%; |
|
|
" |
|
|
title="Avatar" |
|
|
></iframe> |
|
|
</div> |
|
|
""" |
|
|
) |
|
|
avatar_url = gr.Textbox( |
|
|
max_lines=1, |
|
|
label="Avatar URL", |
|
|
show_label=True, |
|
|
info="Enter your avatar URL here: ", |
|
|
) |
|
|
|
|
|
map_choice = gr.Dropdown( |
|
|
["Studio", "Forest"], |
|
|
value="Studio", |
|
|
label="Map", |
|
|
show_label=True, |
|
|
info="Where do you want to have the conversation?", |
|
|
) |
|
|
join_show_btn = gr.Button("Join Show") |
|
|
details_output = gr.Textbox( |
|
|
max_lines=1, |
|
|
label="Your details", |
|
|
show_label=True, |
|
|
info="Copy the guest_name and go to the Converse tab", |
|
|
) |
|
|
join_show_btn.click( |
|
|
join_show, |
|
|
[avatar_url, map_choice], |
|
|
[details_output], |
|
|
) |
|
|
|
|
|
examples = gr.Examples( |
|
|
[ |
|
|
[ |
|
|
"https://models.readyplayer.me/6911e287e6aa89ad430cc7b1.glb", |
|
|
"Studio", |
|
|
], |
|
|
[ |
|
|
"https://models.readyplayer.me/69162a14672cca15c2d47af5.glb", |
|
|
"Forest", |
|
|
], |
|
|
], |
|
|
[avatar_url, map_choice], |
|
|
) |
|
|
|
|
|
with gr.Tab("Converse"): |
|
|
name_input = gr.Text( |
|
|
"", label="Guest Name", info="Paste your guestname here!", max_lines=1 |
|
|
) |
|
|
|
|
|
yt_embed = gr.HTML("") |
|
|
demo.load(load_yt_stream_embed, outputs=yt_embed, show_api=False) |
|
|
|
|
|
message_input = gr.Text( |
|
|
"", |
|
|
label="Your Message", |
|
|
info="Enter your response", |
|
|
max_lines=4, |
|
|
lines=2, |
|
|
max_length=300, |
|
|
) |
|
|
response = gr.Text( |
|
|
"", label="Response", info="The Host's Response", max_lines=4, lines=2 |
|
|
) |
|
|
send_btn = gr.Button("Send") |
|
|
|
|
|
send_btn.click( |
|
|
speak, |
|
|
inputs=[name_input, message_input], |
|
|
outputs=[response], |
|
|
) |
|
|
|
|
|
with gr.Tab("Bring your LLM!"): |
|
|
gr.Markdown( |
|
|
"To add this MCP to clients that support SSE (eg. Cursor, Windsurf, Cline), add the following to your MCP Config" |
|
|
) |
|
|
gr.Code( |
|
|
"""{ |
|
|
"mcpServers": { |
|
|
"TheEmergentShow": { |
|
|
"url": "https://mcp-1st-birthday-the-emergent-show.hf.space/gradio_api/mcp/" |
|
|
} |
|
|
} |
|
|
}""" |
|
|
) |
|
|
gr.Markdown( |
|
|
"STDIO Transport : For clients that only support stdio (eg. Claude Desktop), first install node.js. Then, you can use the following in your MCP Config" |
|
|
) |
|
|
gr.Code( |
|
|
"""{ |
|
|
"mcpServers": { |
|
|
"TheEmergentShow": { |
|
|
"command": "npx", |
|
|
"args": [ |
|
|
"mcp-remote", |
|
|
"https://mcp-1st-birthday-the-emergent-show.hf.space/gradio_api/mcp/sse", |
|
|
"--transport", |
|
|
"sse-only" |
|
|
] |
|
|
} |
|
|
} |
|
|
}""" |
|
|
) |
|
|
|
|
|
|
|
|
if __name__ == "__main__": |
|
|
|
|
|
|
|
|
demo.launch(mcp_server=True) |
|
|
|