Jon Taylor commited on
Commit
bd1c43c
1 Parent(s): 1dae12e

first commit

Browse files
Files changed (7) hide show
  1. .gitignore +158 -0
  2. app/auth.py +26 -0
  3. app/bot.py +161 -0
  4. build-run.sh +2 -0
  5. dockerfile +53 -0
  6. requirements.txt +15 -0
  7. server.py +90 -0
.gitignore ADDED
@@ -0,0 +1,158 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ node_modules/
2
+ .idea/
3
+
4
+ # Byte-compiled / optimized / DLL files
5
+ __pycache__/
6
+ *.py[cod]
7
+ *$py.class
8
+ .DS_Store
9
+
10
+ # C extensions
11
+ *.so
12
+
13
+ # Distribution / packaging
14
+ .Python
15
+ build/
16
+ develop-eggs/
17
+ dist/
18
+ downloads/
19
+ eggs/
20
+ .eggs/
21
+ lib/
22
+ lib64/
23
+ parts/
24
+ sdist/
25
+ var/
26
+ wheels/
27
+ share/python-wheels/
28
+ *.egg-info/
29
+ .installed.cfg
30
+ *.egg
31
+ MANIFEST
32
+
33
+ # PyInstaller
34
+ # Usually these files are written by a python script from a template
35
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
36
+ *.manifest
37
+ *.spec
38
+
39
+ # Installer logs
40
+ pip-log.txt
41
+ pip-delete-this-directory.txt
42
+
43
+ # Unit test / coverage reports
44
+ htmlcov/
45
+ .tox/
46
+ .nox/
47
+ .coverage
48
+ .coverage.*
49
+ .cache
50
+ nosetests.xml
51
+ coverage.xml
52
+ *.cover
53
+ *.py,cover
54
+ .hypothesis/
55
+ .pytest_cache/
56
+ cover/
57
+
58
+ # Translations
59
+ *.mo
60
+ *.pot
61
+
62
+ # Django stuff:
63
+ *.log
64
+ local_settings.py
65
+ db.sqlite3
66
+ db.sqlite3-journal
67
+
68
+ # Flask stuff:
69
+ instance/
70
+ .webassets-cache
71
+
72
+ # Scrapy stuff:
73
+ .scrapy
74
+
75
+ # Sphinx documentation
76
+ docs/_build/
77
+
78
+ # PyBuilder
79
+ .pybuilder/
80
+ target/
81
+
82
+ # Jupyter Notebook
83
+ .ipynb_checkpoints
84
+
85
+ # IPython
86
+ profile_default/
87
+ ipython_config.py
88
+
89
+ # pyenv
90
+ # For a library or package, you might want to ignore these files since the code is
91
+ # intended to run in multiple environments; otherwise, check them in:
92
+ # .python-version
93
+
94
+ # pipenv
95
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
96
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
97
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
98
+ # install all needed dependencies.
99
+ #Pipfile.lock
100
+
101
+ # poetry
102
+ # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
103
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
104
+ # commonly ignored for libraries.
105
+ # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
106
+ #poetry.lock
107
+
108
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow
109
+ __pypackages__/
110
+
111
+ # Celery stuff
112
+ celerybeat-schedule
113
+ celerybeat.pid
114
+
115
+ # SageMath parsed files
116
+ *.sage.py
117
+
118
+ # Environments
119
+ .env
120
+ .venv
121
+ env/
122
+ venv/
123
+ ENV/
124
+ env.bak/
125
+ venv.bak/
126
+
127
+ # Spyder project settings
128
+ .spyderproject
129
+ .spyproject
130
+
131
+ # Rope project settings
132
+ .ropeproject
133
+
134
+ # mkdocs documentation
135
+ /site
136
+
137
+ # mypy
138
+ .mypy_cache/
139
+ .dmypy.json
140
+ dmypy.json
141
+
142
+ # Pyre type checker
143
+ .pyre/
144
+
145
+ # pytype static type analyzer
146
+ .pytype/
147
+
148
+ # Cython debug symbols
149
+ cython_debug/
150
+
151
+ # PyCharm
152
+ # JetBrains specific template is maintainted in a separate JetBrains.gitignore that can
153
+ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
154
+ # and can be added to the global gitignore or merged into this file. For a more nuclear
155
+ # option (not recommended) you can uncomment the following to ignore the entire idea folder.
156
+ #.idea/
157
+
158
+ read.html
app/auth.py ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import time
2
+ import urllib
3
+
4
+ from dotenv import load_dotenv
5
+ import requests
6
+ from flask import jsonify
7
+ import os
8
+
9
+ load_dotenv()
10
+
11
+ def get_meeting_token(room_name, daily_api_key, token_expiry):
12
+ api_path = os.getenv('DAILY_API_PATH') or 'https://api.daily.co/v1'
13
+
14
+ if not token_expiry:
15
+ token_expiry = time.time() + 600
16
+ res = requests.post(f'{api_path}/meeting-tokens',
17
+ headers={'Authorization': f'Bearer {daily_api_key}'},
18
+ json={'properties': {'room_name': room_name, 'is_owner': True, 'exp': token_expiry}})
19
+ if res.status_code != 200:
20
+ return jsonify({'error': 'Unable to create meeting token', 'detail': res.text}), 500
21
+ meeting_token = res.json()['token']
22
+ return meeting_token
23
+
24
+
25
+ def get_room_name(room_url):
26
+ return urllib.parse.urlparse(room_url).path[1:]
app/bot.py ADDED
@@ -0,0 +1,161 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import argparse
2
+ import logging
3
+ import os
4
+ import time
5
+ from threading import Thread
6
+ from typing import Any, Mapping
7
+
8
+ from daily import EventHandler, CallClient, Daily
9
+ from datetime import datetime
10
+ from dotenv import load_dotenv
11
+
12
+ from auth import get_meeting_token, get_room_name
13
+
14
+ load_dotenv()
15
+
16
+ class DailyLLM(EventHandler):
17
+ def __init__(
18
+ self,
19
+ room_url=os.getenv("DAILY_URL"),
20
+ token=os.getenv("DAILY_TOKEN"),
21
+ bot_name="TestBot",
22
+ ):
23
+ duration = os.getenv("BOT_MAX_DURATION")
24
+ if not duration:
25
+ duration = 300
26
+ else:
27
+ duration = int(duration)
28
+ self.expiration = time.time() + duration
29
+
30
+ # room + bot details
31
+ self.room_url = room_url
32
+ room_name = get_room_name(room_url)
33
+ if token:
34
+ self.token = token
35
+ else:
36
+ self.token = get_meeting_token(
37
+ room_name, os.getenv("DAILY_API_KEY"), self.expiration
38
+ )
39
+ self.bot_name = bot_name
40
+
41
+ self.finished_talking_at = None
42
+
43
+ FORMAT = f"%(asctime)s {room_name} %(message)s"
44
+ logging.basicConfig(format=FORMAT)
45
+ self.logger = logging.getLogger("bot-instance")
46
+ self.logger.setLevel(logging.DEBUG)
47
+
48
+ self.logger.info(f"Joining as {self.bot_name}")
49
+ self.logger.info(f"Joining room {self.room_url}")
50
+ self.logger.info(
51
+ f"expiration: {datetime.utcfromtimestamp(self.expiration).strftime('%Y-%m-%d %H:%M:%S')}"
52
+ )
53
+ self.logger.info(
54
+ f"now: {datetime.utcfromtimestamp(time.time()).strftime('%Y-%m-%d %H:%M:%S')}"
55
+ )
56
+
57
+ self.my_participant_id = None
58
+
59
+ self.logger.info("configuring daily")
60
+ self.configure_daily()
61
+
62
+ self.stop_threads = False
63
+ self.image = None
64
+
65
+ #self.logger.info("starting camera thread")
66
+ #self.camera_thread = Thread(target=self.run_camera)
67
+ #self.camera_thread.start()
68
+
69
+ self.participant_left = False
70
+ self.last_fragment_at = None
71
+
72
+ try:
73
+ participant_count = len(self.client.participants())
74
+ self.logger.info(f"{participant_count} participants in room")
75
+ while time.time() < self.expiration and not self.participant_left:
76
+ time.sleep(1)
77
+ except Exception as e:
78
+ self.logger.error(f"Exception {e}")
79
+ finally:
80
+ self.client.leave()
81
+
82
+ self.stop_threads = True
83
+ self.logger.info("Shutting down")
84
+ #self.camera_thread.join()
85
+ #self.logger.info("camera thread stopped")
86
+ #self.logger.info("Services closed.")
87
+
88
+ def configure_daily(self):
89
+ Daily.init()
90
+ self.client = CallClient(event_handler=self)
91
+
92
+ self.mic = Daily.create_microphone_device("mic", sample_rate=16000, channels=1)
93
+ self.speaker = Daily.create_speaker_device(
94
+ "speaker", sample_rate=16000, channels=1
95
+ )
96
+ self.camera = Daily.create_camera_device(
97
+ "camera", width=1024, height=1024, color_format="RGB"
98
+ )
99
+
100
+ Daily.select_speaker_device("speaker")
101
+
102
+ self.client.set_user_name(self.bot_name)
103
+ self.client.join(self.room_url, completion=self.call_joined)
104
+ #self.client.join(self.room_url, self.token, completion=self.call_joined)
105
+
106
+ self.client.update_inputs(
107
+ {
108
+ "camera": {
109
+ "isEnabled": True,
110
+ "settings": {
111
+ "deviceId": "camera",
112
+ "frameRate": 5,
113
+ },
114
+ },
115
+ "microphone": {"isEnabled": True, "settings": {"deviceId": "mic"}},
116
+ }
117
+ )
118
+
119
+ self.my_participant_id = self.client.participants()["local"]["id"]
120
+
121
+ def call_joined(self, join_data, client_error):
122
+ self.logger.info(f"call_joined: {join_data}, {client_error}")
123
+
124
+ def on_participant_joined(self, participant):
125
+ self.logger.info(f"on_participant_joined: {participant}")
126
+ #self.client.send_app_message({"event": "story-id", "storyID": self.story_id})
127
+ self.wave()
128
+ time.sleep(2)
129
+
130
+ def on_participant_left(self, participant, reason):
131
+ if len(self.client.participants()) < 2:
132
+ self.logger.info("participant left")
133
+ self.participant_left = True
134
+
135
+ def wave(self):
136
+ self.client.send_app_message(
137
+ {
138
+ "event": "sync-emoji-reaction",
139
+ "reaction": {
140
+ "emoji": "👋",
141
+ "room": "main-room",
142
+ "sessionId": "bot",
143
+ "id": time.time(),
144
+ },
145
+ }
146
+ )
147
+
148
+
149
+ if __name__ == "__main__":
150
+ parser = argparse.ArgumentParser(description="Daily LLM bot")
151
+ parser.add_argument("-u", "--url", type=str, help="URL of the Daily room")
152
+ parser.add_argument("-t", "--token", type=str, help="Token for Daily API")
153
+ parser.add_argument("-b", "--bot-name", type=str, help="Name of the bot")
154
+
155
+ args = parser.parse_args()
156
+
157
+ url = args.url or os.getenv("DAILY_URL")
158
+ bot_name = args.bot_name or "TestBot"
159
+ token = args.token or None
160
+
161
+ app = DailyLLM(url, token, bot_name)
build-run.sh ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ #!/bin/bash
2
+ gunicorn --workers=2 --log-level info server:app --bind=0.0.0.0:8080
dockerfile ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM nvidia/cuda:12.1.1-cudnn8-devel-ubuntu22.04
2
+
3
+ ARG DEBIAN_FRONTEND=noninteractive
4
+
5
+ ENV PYTHONUNBUFFERED=1
6
+ ENV NODE_MAJOR=20
7
+
8
+ RUN apt-get update && apt-get install --no-install-recommends -y \
9
+ build-essential \
10
+ python3.9 \
11
+ python3-pip \
12
+ python3-dev \
13
+ git \
14
+ ffmpeg \
15
+ google-perftools \
16
+ ca-certificates curl gnupg \
17
+ && apt-get clean && rm -rf /var/lib/apt/lists/*
18
+
19
+ WORKDIR /code
20
+
21
+ RUN mkdir -p /etc/apt/keyrings
22
+ RUN curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg
23
+ RUN echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_${NODE_MAJOR}.x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list > /dev/null
24
+ RUN apt-get update && apt-get install nodejs -y
25
+
26
+ COPY ./requirements.txt /code/requirements.txt
27
+ COPY app/*.py /code/app
28
+
29
+ # Set up a new user named "user" with user ID 1000
30
+ RUN useradd -m -u 1000 user
31
+ # Switch to the "user" user
32
+ USER user
33
+ # Set home to the user's home directory
34
+ ENV HOME=/home/user \
35
+ PATH=/home/user/.local/bin:$PATH \
36
+ PYTHONPATH=$HOME/app \
37
+ PYTHONUNBUFFERED=1 \
38
+ SYSTEM=spaces
39
+
40
+ # Expose Flask port
41
+ ENV FLASK_PORT=8080
42
+ EXPOSE 8080
43
+
44
+ RUN pip3 install --no-cache-dir --upgrade -r /code/requirements.txt
45
+
46
+ # Set the working directory to the user's home directory
47
+ WORKDIR $HOME/app
48
+
49
+ # Copy the current directory contents into the container at $HOME/app setting the owner to the user
50
+ COPY --chown=user . $HOME/app
51
+
52
+ ENV LD_PRELOAD=/usr/lib/x86_64-linux-gnu/libtcmalloc.so.4
53
+ CMD ["./build-run.sh"]
requirements.txt ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ daily-python
2
+ blinker==1.7.0
3
+ click==8.1.7
4
+ Flask==3.0.0
5
+ Flask-Cors==4.0.0
6
+ gunicorn==21.2.0
7
+ itsdangerous==2.1.2
8
+ Jinja2==3.1.2
9
+ MarkupSafe==2.1.3
10
+ packaging==23.2
11
+ Werkzeug==3.0.1
12
+ python-dotenv
13
+ imageio-ffmpeg
14
+ imageio
15
+ requests
server.py ADDED
@@ -0,0 +1,90 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from flask import Flask, jsonify, request
2
+ from flask_cors import CORS
3
+ from dotenv import load_dotenv
4
+ import os
5
+ import requests
6
+ import subprocess
7
+ import time
8
+
9
+ from app.auth import get_meeting_token
10
+
11
+ load_dotenv()
12
+
13
+ app = Flask(__name__)
14
+ CORS(app)
15
+
16
+ def _start_bot(bot_path, args=None):
17
+ daily_api_key = os.getenv("DAILY_API_KEY") or ""
18
+ api_path = os.getenv("DAILY_API_PATH") or "https://api.daily.co/v1"
19
+
20
+ timeout = int(os.getenv("ROOM_TIMEOUT") or os.getenv("BOT_MAX_DURATION") or 300)
21
+ exp = time.time() + timeout
22
+
23
+ '''
24
+ res = requests.post(
25
+ f"{api_path}/rooms",
26
+ headers={"Authorization": f"Bearer {daily_api_key}"},
27
+ json={
28
+ "properties": {
29
+ "exp": exp,
30
+ "enable_chat": True,
31
+ "enable_emoji_reactions": True,
32
+ "eject_at_room_exp": True,
33
+ "enable_prejoin_ui": False,
34
+ }
35
+ },
36
+ )
37
+ if res.status_code != 200:
38
+ return (
39
+ jsonify(
40
+ {
41
+ "error": "Unable to create room",
42
+ "status_code": res.status_code,
43
+ "text": res.text,
44
+ }
45
+ ),
46
+ 500,
47
+ )
48
+ '''
49
+ room_url = os.getenv("DAILY_ROOM_URL") #res.json()["url"]
50
+ room_name = os.getenv("DAILY_ROOM_NAME") #res.json()["name"]
51
+
52
+ meeting_token = get_meeting_token(room_url, daily_api_key, exp)
53
+
54
+ if args:
55
+ extra_args = " ".join([f'-{x[0]} "{x[1]}"' for x in args])
56
+ else:
57
+ extra_args = ""
58
+
59
+ proc = subprocess.Popen(
60
+ [
61
+ f"python {bot_path} -u {room_url} -t {meeting_token} {extra_args}"
62
+ ],
63
+ shell=True,
64
+ bufsize=1,
65
+ )
66
+
67
+ # Don't return until the bot has joined the room, but wait for at most 2 seconds.
68
+ attempts = 0
69
+ while attempts < 20:
70
+ time.sleep(0.1)
71
+ attempts += 1
72
+ res = requests.get(
73
+ f"{api_path}/rooms/{room_name}/get-session-data",
74
+ headers={"Authorization": f"Bearer {daily_api_key}"},
75
+ )
76
+ if res.status_code == 200:
77
+ break
78
+ print(f"Took {attempts} attempts to join room {room_name}")
79
+
80
+ return jsonify({"room_url": room_url, "token": meeting_token}), 200
81
+
82
+ # Routes
83
+ @app.route("/start-bot", methods=["POST"])
84
+ def start_bot():
85
+ return _start_bot("./app/bot.py")
86
+
87
+ @app.route("/")
88
+ def hello_world():
89
+ return "<p>Hello, World!</p>"
90
+