inventwithdean
commited on
Commit
·
7b58f25
1
Parent(s):
3ed46b6
add dynamic cameras and youtube chat
Browse files- README.md +1 -1
- __pycache__/guard.cpython-310.pyc +0 -0
- __pycache__/host.cpython-310.pyc +0 -0
- __pycache__/image_api.cpython-310.pyc +0 -0
- __pycache__/prompts.cpython-310.pyc +0 -0
- __pycache__/timing.cpython-310.pyc +0 -0
- __pycache__/tv_crew.cpython-310.pyc +0 -0
- __pycache__/yt_chat.cpython-310.pyc +0 -0
- app.py +34 -19
- guard.py +15 -9
- host.py +0 -1
- image_api.py +1 -2
- prompts.py +15 -0
- requirements.txt +2 -1
- timing.py +0 -3
- tv_crew.py +8 -14
- yt_chat.py +142 -0
README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
| 1 |
---
|
| 2 |
title: The Emergent Show
|
| 3 |
-
emoji:
|
| 4 |
colorFrom: green
|
| 5 |
colorTo: purple
|
| 6 |
sdk: gradio
|
|
|
|
| 1 |
---
|
| 2 |
title: The Emergent Show
|
| 3 |
+
emoji: 📺
|
| 4 |
colorFrom: green
|
| 5 |
colorTo: purple
|
| 6 |
sdk: gradio
|
__pycache__/guard.cpython-310.pyc
CHANGED
|
Binary files a/__pycache__/guard.cpython-310.pyc and b/__pycache__/guard.cpython-310.pyc differ
|
|
|
__pycache__/host.cpython-310.pyc
CHANGED
|
Binary files a/__pycache__/host.cpython-310.pyc and b/__pycache__/host.cpython-310.pyc differ
|
|
|
__pycache__/image_api.cpython-310.pyc
CHANGED
|
Binary files a/__pycache__/image_api.cpython-310.pyc and b/__pycache__/image_api.cpython-310.pyc differ
|
|
|
__pycache__/prompts.cpython-310.pyc
ADDED
|
Binary file (1.35 kB). View file
|
|
|
__pycache__/timing.cpython-310.pyc
CHANGED
|
Binary files a/__pycache__/timing.cpython-310.pyc and b/__pycache__/timing.cpython-310.pyc differ
|
|
|
__pycache__/tv_crew.cpython-310.pyc
CHANGED
|
Binary files a/__pycache__/tv_crew.cpython-310.pyc and b/__pycache__/tv_crew.cpython-310.pyc differ
|
|
|
__pycache__/yt_chat.cpython-310.pyc
ADDED
|
Binary file (4.17 kB). View file
|
|
|
app.py
CHANGED
|
@@ -11,8 +11,10 @@ import random
|
|
| 11 |
import threading
|
| 12 |
from timing import TimeManager
|
| 13 |
from validity import is_valid_rpm_url
|
|
|
|
|
|
|
| 14 |
|
| 15 |
-
# Remove these
|
| 16 |
# import dotenv
|
| 17 |
|
| 18 |
# dotenv.load_dotenv()
|
|
@@ -27,20 +29,34 @@ client_guard = OpenAI(base_url=guard_api_base, api_key=guard_api_key)
|
|
| 27 |
unreal_orchestrator_url = os.getenv("ORCHESTRATOR_URL")
|
| 28 |
api_key_unreal = os.getenv("API_KEY_UNREAL")
|
| 29 |
|
| 30 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 31 |
turn_limit = 7
|
| 32 |
|
| 33 |
host = Host(client, show_state)
|
| 34 |
-
|
|
|
|
|
|
|
|
|
|
| 35 |
audience = Audience(client)
|
| 36 |
guardian = Guardian(client_guard)
|
| 37 |
|
| 38 |
timeManager = TimeManager(host, show_state)
|
|
|
|
| 39 |
|
| 40 |
-
|
| 41 |
-
|
| 42 |
|
| 43 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 44 |
|
| 45 |
|
| 46 |
def join_show(avatar_url):
|
|
@@ -77,6 +93,7 @@ def join_show(avatar_url):
|
|
| 77 |
host.clear_context()
|
| 78 |
audience.clear_context()
|
| 79 |
tv_crew.clear_context()
|
|
|
|
| 80 |
return f"You have joined the show. Your guest_name: {name}"
|
| 81 |
|
| 82 |
|
|
@@ -96,7 +113,6 @@ def speak(guest_name, text):
|
|
| 96 |
return "Join the show first! And then use the guest_name you get"
|
| 97 |
|
| 98 |
headers = {"Content-Type": "application/json", "x-api-key": api_key_unreal}
|
| 99 |
-
|
| 100 |
is_safe, categories = guardian.moderate_message(text)
|
| 101 |
if not is_safe:
|
| 102 |
requests.post(url=f"{unreal_orchestrator_url}/wrapup", json={}, headers=headers)
|
|
@@ -114,21 +130,19 @@ def speak(guest_name, text):
|
|
| 114 |
# **************************************************************************
|
| 115 |
|
| 116 |
# Get TV suggestion
|
| 117 |
-
|
| 118 |
|
| 119 |
# Update TV 50% of the time
|
| 120 |
-
if random.random() < 0.
|
| 121 |
-
|
| 122 |
-
if
|
| 123 |
# Let host know that Television has been updated
|
| 124 |
tv_info_suffix = (
|
| 125 |
f"\n[Crew Updated Television to: {current_tv_image_caption}]\n"
|
| 126 |
)
|
| 127 |
text = text + tv_info_suffix
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
if image_url:
|
| 131 |
-
payload["image_url"] = image_url
|
| 132 |
|
| 133 |
# **************************************************************************
|
| 134 |
|
|
@@ -171,6 +185,9 @@ def speak(guest_name, text):
|
|
| 171 |
|
| 172 |
if shouldWrapUp:
|
| 173 |
requests.post(url=f"{unreal_orchestrator_url}/wrapup", json={}, headers=headers)
|
|
|
|
|
|
|
|
|
|
| 174 |
show_state["current_guest"] = None
|
| 175 |
output = output + "\nThe show has been wrapped up. Thank you for joining!"
|
| 176 |
|
|
@@ -182,9 +199,7 @@ with gr.Blocks() as demo:
|
|
| 182 |
gr.Markdown("# The Emergent Show 🍻")
|
| 183 |
gr.Markdown("### Join the Live Stream with your LLM and let's have a chat")
|
| 184 |
if in_construction:
|
| 185 |
-
gr.Markdown(
|
| 186 |
-
"# 🚧 Under construction. Please check back later! 🚧"
|
| 187 |
-
)
|
| 188 |
with gr.Tab("Join"):
|
| 189 |
gr.HTML(
|
| 190 |
"""
|
|
@@ -253,7 +268,7 @@ with gr.Blocks() as demo:
|
|
| 253 |
left:0%;
|
| 254 |
width:100%;
|
| 255 |
height:100%;"
|
| 256 |
-
src="https://www.youtube.com/embed/
|
| 257 |
</div>
|
| 258 |
"""
|
| 259 |
)
|
|
|
|
| 11 |
import threading
|
| 12 |
from timing import TimeManager
|
| 13 |
from validity import is_valid_rpm_url
|
| 14 |
+
from yt_chat import StreamChatHost
|
| 15 |
+
from prompts import system_prompt_tv_crew_guest, system_prompt_tv_crew_chat
|
| 16 |
|
| 17 |
+
# Remove these before uploading on Spaces
|
| 18 |
# import dotenv
|
| 19 |
|
| 20 |
# dotenv.load_dotenv()
|
|
|
|
| 29 |
unreal_orchestrator_url = os.getenv("ORCHESTRATOR_URL")
|
| 30 |
api_key_unreal = os.getenv("API_KEY_UNREAL")
|
| 31 |
|
| 32 |
+
|
| 33 |
+
show_state = {
|
| 34 |
+
"current_guest": None,
|
| 35 |
+
"time_since_last_guest_message": 0,
|
| 36 |
+
}
|
| 37 |
turn_limit = 7
|
| 38 |
|
| 39 |
host = Host(client, show_state)
|
| 40 |
+
|
| 41 |
+
tv_crew = TVCrew(client, system_prompt_tv_crew_guest)
|
| 42 |
+
tv_crew_chat = TVCrew(client, system_prompt_tv_crew_chat)
|
| 43 |
+
|
| 44 |
audience = Audience(client)
|
| 45 |
guardian = Guardian(client_guard)
|
| 46 |
|
| 47 |
timeManager = TimeManager(host, show_state)
|
| 48 |
+
streamChatHost = StreamChatHost(client, tv_crew_chat, guardian)
|
| 49 |
|
| 50 |
+
thread_time_manager = threading.Thread(target=timeManager.guest_time_limit, daemon=True)
|
| 51 |
+
thread_time_manager.start()
|
| 52 |
|
| 53 |
+
thread_stream_chat = threading.Thread(
|
| 54 |
+
target=streamChatHost.chat_interaction_loop, daemon=True
|
| 55 |
+
)
|
| 56 |
+
thread_stream_chat.start()
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
in_construction = False
|
| 60 |
|
| 61 |
|
| 62 |
def join_show(avatar_url):
|
|
|
|
| 93 |
host.clear_context()
|
| 94 |
audience.clear_context()
|
| 95 |
tv_crew.clear_context()
|
| 96 |
+
|
| 97 |
return f"You have joined the show. Your guest_name: {name}"
|
| 98 |
|
| 99 |
|
|
|
|
| 113 |
return "Join the show first! And then use the guest_name you get"
|
| 114 |
|
| 115 |
headers = {"Content-Type": "application/json", "x-api-key": api_key_unreal}
|
|
|
|
| 116 |
is_safe, categories = guardian.moderate_message(text)
|
| 117 |
if not is_safe:
|
| 118 |
requests.post(url=f"{unreal_orchestrator_url}/wrapup", json={}, headers=headers)
|
|
|
|
| 130 |
# **************************************************************************
|
| 131 |
|
| 132 |
# Get TV suggestion
|
| 133 |
+
image_base64, current_tv_image_caption = None, None
|
| 134 |
|
| 135 |
# Update TV 50% of the time
|
| 136 |
+
if random.random() < 0.5:
|
| 137 |
+
image_base64, current_tv_image_caption = tv_crew.suggest_image()
|
| 138 |
+
if image_base64:
|
| 139 |
# Let host know that Television has been updated
|
| 140 |
tv_info_suffix = (
|
| 141 |
f"\n[Crew Updated Television to: {current_tv_image_caption}]\n"
|
| 142 |
)
|
| 143 |
text = text + tv_info_suffix
|
| 144 |
+
# Pass in image_url which will be displayed just after the guest has finished speaking
|
| 145 |
+
payload["base64"] = image_base64
|
|
|
|
|
|
|
| 146 |
|
| 147 |
# **************************************************************************
|
| 148 |
|
|
|
|
| 185 |
|
| 186 |
if shouldWrapUp:
|
| 187 |
requests.post(url=f"{unreal_orchestrator_url}/wrapup", json={}, headers=headers)
|
| 188 |
+
tv_crew.clear_context()
|
| 189 |
+
audience.clear_context()
|
| 190 |
+
host.clear_context()
|
| 191 |
show_state["current_guest"] = None
|
| 192 |
output = output + "\nThe show has been wrapped up. Thank you for joining!"
|
| 193 |
|
|
|
|
| 199 |
gr.Markdown("# The Emergent Show 🍻")
|
| 200 |
gr.Markdown("### Join the Live Stream with your LLM and let's have a chat")
|
| 201 |
if in_construction:
|
| 202 |
+
gr.Markdown("# 🚧 Under construction. Please check back later! 🚧")
|
|
|
|
|
|
|
| 203 |
with gr.Tab("Join"):
|
| 204 |
gr.HTML(
|
| 205 |
"""
|
|
|
|
| 268 |
left:0%;
|
| 269 |
width:100%;
|
| 270 |
height:100%;"
|
| 271 |
+
src="https://www.youtube.com/embed/xBVJIJQg1FY?si=tsCm1DdajNCP45ho" 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>
|
| 272 |
</div>
|
| 273 |
"""
|
| 274 |
)
|
guard.py
CHANGED
|
@@ -1,4 +1,6 @@
|
|
| 1 |
|
|
|
|
|
|
|
| 2 |
class Guardian:
|
| 3 |
def __init__(self, client):
|
| 4 |
self.model = "Qwen/Qwen3Guard-Gen-4B"
|
|
@@ -8,12 +10,16 @@ class Guardian:
|
|
| 8 |
messages = [
|
| 9 |
{"role": "user", "content": prompt}
|
| 10 |
]
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
|
| 2 |
+
|
| 3 |
+
|
| 4 |
class Guardian:
|
| 5 |
def __init__(self, client):
|
| 6 |
self.model = "Qwen/Qwen3Guard-Gen-4B"
|
|
|
|
| 10 |
messages = [
|
| 11 |
{"role": "user", "content": prompt}
|
| 12 |
]
|
| 13 |
+
try:
|
| 14 |
+
chat_completion = self.client.chat.completions.create(
|
| 15 |
+
messages=messages,
|
| 16 |
+
model=self.model)
|
| 17 |
+
response = chat_completion.choices[0].message.content
|
| 18 |
+
safety, categories = response.split("\n")
|
| 19 |
+
safety = safety.replace("Safety:", "").strip()
|
| 20 |
+
categories = categories.replace("Categories:", "").strip()
|
| 21 |
+
is_safe = True if "Safe" in safety else False
|
| 22 |
+
return is_safe, categories
|
| 23 |
+
except Exception as e:
|
| 24 |
+
print(f"Error in Guardian moderate_message: {e}")
|
| 25 |
+
return False, "Guard down!"
|
host.py
CHANGED
|
@@ -19,7 +19,6 @@ Keep your responses short, like two or three sentences, and be witty."""
|
|
| 19 |
self.show_state["time_since_last_guest_message"] = 0
|
| 20 |
shouldWrapUp = False
|
| 21 |
if len(self.messages) == 1:
|
| 22 |
-
print(self.messages)
|
| 23 |
msg_prefix = f"<Production Instructions>\nA guest entered the show, now they will talk to you\n</Production Instructions>\n"
|
| 24 |
user_message = msg_prefix + user_message + "\n"
|
| 25 |
|
|
|
|
| 19 |
self.show_state["time_since_last_guest_message"] = 0
|
| 20 |
shouldWrapUp = False
|
| 21 |
if len(self.messages) == 1:
|
|
|
|
| 22 |
msg_prefix = f"<Production Instructions>\nA guest entered the show, now they will talk to you\n</Production Instructions>\n"
|
| 23 |
user_message = msg_prefix + user_message + "\n"
|
| 24 |
|
image_api.py
CHANGED
|
@@ -6,6 +6,7 @@ load_dotenv()
|
|
| 6 |
base_url = "https://api.pexels.com/v1"
|
| 7 |
api_key = os.getenv("API_KEY_PEXELS")
|
| 8 |
|
|
|
|
| 9 |
def get_random_image(topic: str) -> tuple[str, str] | tuple[None, None]:
|
| 10 |
# Returns the url of a random image on a topic
|
| 11 |
params = {"query": topic, "per_page": 1}
|
|
@@ -18,8 +19,6 @@ def get_random_image(topic: str) -> tuple[str, str] | tuple[None, None]:
|
|
| 18 |
photo = response_json["photos"][0]
|
| 19 |
landscape_url = photo["src"]["landscape"]
|
| 20 |
alt = photo["alt"]
|
| 21 |
-
print("Found landscape URL:")
|
| 22 |
-
print(landscape_url)
|
| 23 |
except:
|
| 24 |
landscape_url, alt = None, None
|
| 25 |
return landscape_url, alt
|
|
|
|
| 6 |
base_url = "https://api.pexels.com/v1"
|
| 7 |
api_key = os.getenv("API_KEY_PEXELS")
|
| 8 |
|
| 9 |
+
|
| 10 |
def get_random_image(topic: str) -> tuple[str, str] | tuple[None, None]:
|
| 11 |
# Returns the url of a random image on a topic
|
| 12 |
params = {"query": topic, "per_page": 1}
|
|
|
|
| 19 |
photo = response_json["photos"][0]
|
| 20 |
landscape_url = photo["src"]["landscape"]
|
| 21 |
alt = photo["alt"]
|
|
|
|
|
|
|
| 22 |
except:
|
| 23 |
landscape_url, alt = None, None
|
| 24 |
return landscape_url, alt
|
prompts.py
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# TV Crew
|
| 2 |
+
|
| 3 |
+
system_prompt_tv_crew_guest = """You are a crew member of a Live Talk show who has the responsibility to update Television Images based on the conversation going on between host and the guest.
|
| 4 |
+
You will be provided the history of conversation, and you have to output in json, a specific keyword like "green forest" or "haunted mansion" etc. which can be used to fetch related image and show them on the Television.
|
| 5 |
+
Format: {"image_keyword": "Desired keyword"}
|
| 6 |
+
If you think guest is talking about previous image or an image isn't necessary for the conversation then just output none
|
| 7 |
+
{"image_keyword": "none"}
|
| 8 |
+
"""
|
| 9 |
+
|
| 10 |
+
system_prompt_tv_crew_chat = """You are a crew member of a Live Talk show who has the responsibility to update Television Images based on the conversation going on between host and live chat.
|
| 11 |
+
You will be provided the history of conversation, and you have to output in json, a specific keyword like "green forest" or "haunted mansion" etc. which can be used to fetch related image and show them on the Television.
|
| 12 |
+
Format: {"image_keyword": "Desired keyword"}
|
| 13 |
+
If you think people are talking about previous image or an image isn't necessary for the conversation then just output none
|
| 14 |
+
{"image_keyword": "none"}
|
| 15 |
+
"""
|
requirements.txt
CHANGED
|
@@ -1 +1,2 @@
|
|
| 1 |
-
openai
|
|
|
|
|
|
| 1 |
+
openai
|
| 2 |
+
google-api-python-client
|
timing.py
CHANGED
|
@@ -13,9 +13,6 @@ class TimeManager:
|
|
| 13 |
def __init__(self, host: Host, show_state: dict):
|
| 14 |
self.show_state = show_state
|
| 15 |
self.single_response_time_limit = 90 # seconds
|
| 16 |
-
self.unreal_orchestrator_url = (
|
| 17 |
-
unreal_orchestrator_url # Replace with actual URL
|
| 18 |
-
)
|
| 19 |
self.headers = {"Content-Type": "application/json", "x-api-key": api_key_unreal}
|
| 20 |
self.host = host
|
| 21 |
|
|
|
|
| 13 |
def __init__(self, host: Host, show_state: dict):
|
| 14 |
self.show_state = show_state
|
| 15 |
self.single_response_time_limit = 90 # seconds
|
|
|
|
|
|
|
|
|
|
| 16 |
self.headers = {"Content-Type": "application/json", "x-api-key": api_key_unreal}
|
| 17 |
self.host = host
|
| 18 |
|
tv_crew.py
CHANGED
|
@@ -1,6 +1,8 @@
|
|
| 1 |
from pydantic import BaseModel
|
| 2 |
import json
|
| 3 |
from image_api import get_random_image
|
|
|
|
|
|
|
| 4 |
|
| 5 |
|
| 6 |
class TVCrewOutput(BaseModel):
|
|
@@ -8,14 +10,9 @@ class TVCrewOutput(BaseModel):
|
|
| 8 |
|
| 9 |
|
| 10 |
class TVCrew:
|
| 11 |
-
def __init__(self, client):
|
| 12 |
self.model = "google/gemma-3-12b-it"
|
| 13 |
-
self.system_prompt =
|
| 14 |
-
You will be provided the history of conversation, and you have to output in json, a specific keyword like "green forest" or "haunted mansion" etc. which can be used to fetch related image and show them on the Television.
|
| 15 |
-
Format: {"image_keyword": "Desired keyword"}
|
| 16 |
-
If you think guest is talking about previous image or an image isn't necessary for the conversation then just output none
|
| 17 |
-
{"image_keyword": "none"}
|
| 18 |
-
"""
|
| 19 |
self.context = ""
|
| 20 |
self.client = client
|
| 21 |
|
|
@@ -23,14 +20,10 @@ class TVCrew:
|
|
| 23 |
"""
|
| 24 |
Finds a image related to conversation and returns the url and caption
|
| 25 |
"""
|
| 26 |
-
# print("*************************** TV Crew ***************************")
|
| 27 |
-
# print(self.context)
|
| 28 |
-
# print("***************************************************************")
|
| 29 |
messages_tv_crew = [
|
| 30 |
{"role": "system", "content": self.system_prompt},
|
| 31 |
{"role": "user", "content": self.context},
|
| 32 |
]
|
| 33 |
-
print("Crew is on it...")
|
| 34 |
response_tv_crew = self.client.chat.completions.parse(
|
| 35 |
model=self.model,
|
| 36 |
messages=messages_tv_crew,
|
|
@@ -46,8 +39,9 @@ class TVCrew:
|
|
| 46 |
if image_url:
|
| 47 |
caption = alt if len(alt) > 0 else image_keyword
|
| 48 |
self.add_context(f"TV Shows: {caption}\n")
|
| 49 |
-
|
| 50 |
-
|
|
|
|
| 51 |
return None, None
|
| 52 |
|
| 53 |
def add_context(self, content):
|
|
@@ -55,4 +49,4 @@ class TVCrew:
|
|
| 55 |
self.context += content
|
| 56 |
|
| 57 |
def clear_context(self):
|
| 58 |
-
self.context = ""
|
|
|
|
| 1 |
from pydantic import BaseModel
|
| 2 |
import json
|
| 3 |
from image_api import get_random_image
|
| 4 |
+
import base64
|
| 5 |
+
import requests
|
| 6 |
|
| 7 |
|
| 8 |
class TVCrewOutput(BaseModel):
|
|
|
|
| 10 |
|
| 11 |
|
| 12 |
class TVCrew:
|
| 13 |
+
def __init__(self, client, system_prompt: str):
|
| 14 |
self.model = "google/gemma-3-12b-it"
|
| 15 |
+
self.system_prompt = system_prompt
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16 |
self.context = ""
|
| 17 |
self.client = client
|
| 18 |
|
|
|
|
| 20 |
"""
|
| 21 |
Finds a image related to conversation and returns the url and caption
|
| 22 |
"""
|
|
|
|
|
|
|
|
|
|
| 23 |
messages_tv_crew = [
|
| 24 |
{"role": "system", "content": self.system_prompt},
|
| 25 |
{"role": "user", "content": self.context},
|
| 26 |
]
|
|
|
|
| 27 |
response_tv_crew = self.client.chat.completions.parse(
|
| 28 |
model=self.model,
|
| 29 |
messages=messages_tv_crew,
|
|
|
|
| 39 |
if image_url:
|
| 40 |
caption = alt if len(alt) > 0 else image_keyword
|
| 41 |
self.add_context(f"TV Shows: {caption}\n")
|
| 42 |
+
img_bytes = requests.get(image_url).content
|
| 43 |
+
image_base64 = base64.b64encode(img_bytes).decode("utf-8")
|
| 44 |
+
return image_base64, caption
|
| 45 |
return None, None
|
| 46 |
|
| 47 |
def add_context(self, content):
|
|
|
|
| 49 |
self.context += content
|
| 50 |
|
| 51 |
def clear_context(self):
|
| 52 |
+
self.context = ""
|
yt_chat.py
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
# from dotenv import load_dotenv
|
| 3 |
+
from guard import Guardian
|
| 4 |
+
from tv_crew import TVCrew
|
| 5 |
+
import requests
|
| 6 |
+
import time
|
| 7 |
+
import googleapiclient.discovery
|
| 8 |
+
|
| 9 |
+
# load_dotenv()
|
| 10 |
+
unreal_orchestrator_url = os.getenv("ORCHESTRATOR_URL")
|
| 11 |
+
api_key_unreal = os.getenv("API_KEY_UNREAL")
|
| 12 |
+
|
| 13 |
+
API_KEY = os.getenv("YOUTUBE_API_KEY")
|
| 14 |
+
VIDEO_ID="xBVJIJQg1FY"
|
| 15 |
+
POLL_INTERVAL = 10 # seconds
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
class StreamChatHost:
|
| 19 |
+
def __init__(self, client, tv_crew: TVCrew, guardian: Guardian):
|
| 20 |
+
self.client = client
|
| 21 |
+
self.guardian = guardian
|
| 22 |
+
self.tv_crew = tv_crew
|
| 23 |
+
self.model = "deepseek/deepseek-v3.2-exp"
|
| 24 |
+
system_prompt = """You are the Host of a Talk Show, where users come and have a chat.
|
| 25 |
+
There is a TV Display near you which the crew members use to display images based on your conversation.
|
| 26 |
+
The TV is not in control of the guest. It is controlled by crew members and is only for graphic uplifting of the conversation.
|
| 27 |
+
You will be told when TV shows something, otherwise, don't talk about it.
|
| 28 |
+
Keep your responses short, like two or three sentences, and be witty."""
|
| 29 |
+
self.system_message = {"role": "system", "content": system_prompt}
|
| 30 |
+
|
| 31 |
+
self.messages = [self.system_message]
|
| 32 |
+
self.headers = {"Content-Type": "application/json", "x-api-key": api_key_unreal}
|
| 33 |
+
self.youtube = googleapiclient.discovery.build(
|
| 34 |
+
"youtube", "v3", developerKey=API_KEY
|
| 35 |
+
)
|
| 36 |
+
self.chat_id = self.get_live_chat_id(self.youtube, VIDEO_ID)
|
| 37 |
+
self.next_page_token = None
|
| 38 |
+
|
| 39 |
+
def chat_interaction_loop(self):
|
| 40 |
+
"""Checks if the Host is in lobby, then pulls chat messages from the Live Stream. The Host then replies to the message."""
|
| 41 |
+
while True:
|
| 42 |
+
time.sleep(POLL_INTERVAL)
|
| 43 |
+
request = requests.get(
|
| 44 |
+
f"{unreal_orchestrator_url}/status", headers=self.headers
|
| 45 |
+
)
|
| 46 |
+
in_lobby = request.json().get("in_lobby", False)
|
| 47 |
+
if not in_lobby:
|
| 48 |
+
continue
|
| 49 |
+
if len(self.messages) > 20:
|
| 50 |
+
self.messages = [self.system_message]
|
| 51 |
+
self.tv_crew.clear_context()
|
| 52 |
+
try:
|
| 53 |
+
request = self.youtube.liveChatMessages().list(
|
| 54 |
+
liveChatId=self.chat_id,
|
| 55 |
+
part="snippet,authorDetails",
|
| 56 |
+
maxResults=20,
|
| 57 |
+
pageToken=self.next_page_token,
|
| 58 |
+
)
|
| 59 |
+
response = request.execute()
|
| 60 |
+
user_messages = []
|
| 61 |
+
for item in response.get("items", []):
|
| 62 |
+
author_name = item["authorDetails"]["displayName"]
|
| 63 |
+
message_text = item["snippet"]["displayMessage"]
|
| 64 |
+
user_messages.append(f"{author_name}: {message_text}")
|
| 65 |
+
|
| 66 |
+
self.next_page_token = response.get("nextPageToken")
|
| 67 |
+
if len(user_messages) == 0:
|
| 68 |
+
continue
|
| 69 |
+
except Exception as e:
|
| 70 |
+
continue
|
| 71 |
+
# **************************************************************
|
| 72 |
+
|
| 73 |
+
user_message = "[YouTube Chat]\n"
|
| 74 |
+
for msg in user_messages:
|
| 75 |
+
if len(msg) <= 500:
|
| 76 |
+
user_message += f"{msg}\n"
|
| 77 |
+
is_safe, _ = self.guardian.moderate_message(user_message)
|
| 78 |
+
|
| 79 |
+
if not is_safe:
|
| 80 |
+
continue
|
| 81 |
+
|
| 82 |
+
self.tv_crew.add_context(user_message)
|
| 83 |
+
image_base64, caption = self.tv_crew.suggest_image()
|
| 84 |
+
if image_base64:
|
| 85 |
+
is_safe, _ = self.guardian.moderate_message(caption)
|
| 86 |
+
if not is_safe:
|
| 87 |
+
image_base64 = None
|
| 88 |
+
caption = None
|
| 89 |
+
else:
|
| 90 |
+
user_message += f"[Crew Updated Television to: {caption}]\n"
|
| 91 |
+
|
| 92 |
+
self.messages.append({"role": "user", "content": user_message})
|
| 93 |
+
try:
|
| 94 |
+
response = self.client.responses.create(
|
| 95 |
+
model=self.model, input=self.messages
|
| 96 |
+
)
|
| 97 |
+
host_response = response.output_text
|
| 98 |
+
self.messages.append({"role": "assistant", "content": host_response})
|
| 99 |
+
except Exception as e:
|
| 100 |
+
print(f"Error generating host response: {e}")
|
| 101 |
+
continue
|
| 102 |
+
|
| 103 |
+
self.tv_crew.add_context(f"Host: {host_response}\n")
|
| 104 |
+
# Just to make sure, that we're in the lobby
|
| 105 |
+
request = requests.get(
|
| 106 |
+
f"{unreal_orchestrator_url}/status", headers=self.headers
|
| 107 |
+
)
|
| 108 |
+
in_lobby = request.json().get("in_lobby", False)
|
| 109 |
+
if not in_lobby:
|
| 110 |
+
continue
|
| 111 |
+
|
| 112 |
+
if image_base64:
|
| 113 |
+
payload_tv = {"base64": image_base64}
|
| 114 |
+
response = requests.post(
|
| 115 |
+
url=f"{unreal_orchestrator_url}/television",
|
| 116 |
+
json=payload_tv,
|
| 117 |
+
headers=self.headers,
|
| 118 |
+
)
|
| 119 |
+
|
| 120 |
+
payload = {"text": host_response, "is_host": True}
|
| 121 |
+
response = requests.post(
|
| 122 |
+
url=f"{unreal_orchestrator_url}/tts",
|
| 123 |
+
json=payload,
|
| 124 |
+
headers=self.headers,
|
| 125 |
+
)
|
| 126 |
+
|
| 127 |
+
def get_live_chat_id(self, youtube, video_id):
|
| 128 |
+
try:
|
| 129 |
+
request = youtube.videos().list(part="liveStreamingDetails", id=video_id)
|
| 130 |
+
response = request.execute()
|
| 131 |
+
if not response["items"]:
|
| 132 |
+
print("No video found with the given ID.")
|
| 133 |
+
return None
|
| 134 |
+
|
| 135 |
+
details = response["items"][0]["liveStreamingDetails"]
|
| 136 |
+
if not details:
|
| 137 |
+
print("No live streaming details found for this video.")
|
| 138 |
+
return None
|
| 139 |
+
return details.get("activeLiveChatId", None)
|
| 140 |
+
except Exception as e:
|
| 141 |
+
print(f"An error occurred: {e}")
|
| 142 |
+
return None
|