SakibAhmed commited on
Commit
efd7f96
·
verified ·
1 Parent(s): bf4595d

Upload 2 files

Browse files
Files changed (2) hide show
  1. server.py +20 -16
  2. templates/index.html +107 -16
server.py CHANGED
@@ -37,7 +37,7 @@ def index():
37
 
38
  @app.route("/get-token", methods=["GET"])
39
  def get_token():
40
- """Generates a Livekit token for a user to join the room."""
41
  identity = f"user-{uuid.uuid4()}"
42
 
43
  # Create a token with permissions to join a specific room
@@ -45,32 +45,29 @@ def get_token():
45
  api.AccessToken(LIVEKIT_API_KEY, LIVEKIT_API_SECRET)
46
  .with_identity(identity)
47
  .with_name(f"Visitor-{identity}")
48
- # CORRECTED: api.VideoGrant changed to api.VideoGrants
49
  .with_grants(api.VideoGrants(room_join=True, room=ROOM_NAME))
50
  )
51
 
52
- return flask.jsonify({"token": token.to_jwt()})
 
 
 
 
53
 
54
 
55
  def run_agent_worker():
56
  """
57
  Runs the app.py agent worker in a subprocess.
58
- This worker needs its own token to connect to the room.
59
  """
60
  print("Starting Livekit Agent worker...")
61
 
62
- # The agent also needs a token to join the room
63
  agent_token = (
64
  api.AccessToken(LIVEKIT_API_KEY, LIVEKIT_API_SECRET)
65
  .with_identity("devraze-agent")
66
  .with_name("DevRaze")
67
- # CORRECTED: api.VideoGrant changed to api.VideoGrants
68
  .with_grants(api.VideoGrants(room_join=True, room=ROOM_NAME, room_admin=True, hidden=True))
69
  )
70
 
71
- # The `livekit-agent` CLI is the standard way to run agent workers
72
- # It takes the URL and token as arguments.
73
- # We pass the module and class name of our agent.
74
  command = [
75
  "livekit-agent",
76
  "run-agent",
@@ -82,14 +79,21 @@ def run_agent_worker():
82
  ]
83
 
84
  try:
85
- # Using subprocess.run to block and see output for debugging
86
- # In a real production scenario, you might use Popen for non-blocking
87
- subprocess.run(command, check=True)
88
- except subprocess.CalledProcessError as e:
89
- print(f"Agent worker failed with error: {e}")
 
 
 
 
 
 
90
  except FileNotFoundError:
91
  print("Error: 'livekit-agent' command not found.")
92
- print("Please make sure you have installed the livekit-agents package correctly.")
 
93
 
94
 
95
  if __name__ == "__main__":
@@ -98,5 +102,5 @@ if __name__ == "__main__":
98
  agent_thread.daemon = True
99
  agent_thread.start()
100
 
101
- # Run the Flask app for the frontend and token generation
102
  app.run(host="0.0.0.0", port=7860)
 
37
 
38
  @app.route("/get-token", methods=["GET"])
39
  def get_token():
40
+ """Generates a Livekit token and returns it with the correct WS URL."""
41
  identity = f"user-{uuid.uuid4()}"
42
 
43
  # Create a token with permissions to join a specific room
 
45
  api.AccessToken(LIVEKIT_API_KEY, LIVEKIT_API_SECRET)
46
  .with_identity(identity)
47
  .with_name(f"Visitor-{identity}")
 
48
  .with_grants(api.VideoGrants(room_join=True, room=ROOM_NAME))
49
  )
50
 
51
+ # FIX: Return both the token AND the Livekit URL from the environment
52
+ return flask.jsonify({
53
+ "token": token.to_jwt(),
54
+ "livekitUrl": LIVEKIT_URL
55
+ })
56
 
57
 
58
  def run_agent_worker():
59
  """
60
  Runs the app.py agent worker in a subprocess.
 
61
  """
62
  print("Starting Livekit Agent worker...")
63
 
 
64
  agent_token = (
65
  api.AccessToken(LIVEKIT_API_KEY, LIVEKIT_API_SECRET)
66
  .with_identity("devraze-agent")
67
  .with_name("DevRaze")
 
68
  .with_grants(api.VideoGrants(room_join=True, room=ROOM_NAME, room_admin=True, hidden=True))
69
  )
70
 
 
 
 
71
  command = [
72
  "livekit-agent",
73
  "run-agent",
 
79
  ]
80
 
81
  try:
82
+ # Using Popen instead of run so it doesn't block, but streams output
83
+ process = subprocess.Popen(
84
+ command,
85
+ stdout=subprocess.PIPE,
86
+ stderr=subprocess.STDOUT,
87
+ text=True
88
+ )
89
+ # Print agent logs to the main console
90
+ for line in process.stdout:
91
+ print(f"[AGENT] {line.strip()}")
92
+
93
  except FileNotFoundError:
94
  print("Error: 'livekit-agent' command not found.")
95
+ except Exception as e:
96
+ print(f"Error starting agent: {e}")
97
 
98
 
99
  if __name__ == "__main__":
 
102
  agent_thread.daemon = True
103
  agent_thread.start()
104
 
105
+ # Run the Flask app
106
  app.run(host="0.0.0.0", port=7860)
templates/index.html CHANGED
@@ -60,6 +60,7 @@
60
  padding: 10px;
61
  border-radius: 4px;
62
  transition: all 0.3s ease;
 
63
  }
64
 
65
  .status-disconnected {
@@ -103,23 +104,48 @@
103
  color: #888;
104
  cursor: not-allowed;
105
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
106
  </style>
107
  </head>
108
  <body>
109
  <div class="container">
110
  <h1>DevRaze, the Roastmaster AI</h1>
111
- <p>Embedded in Rajesh Yarra’s portfolio. Ask me anything about him. I dare you. I promise it won't crash... probably.</p>
112
- <div id="status" class="status-disconnected">Status: Disconnected</div>
113
  <button id="connect-button">Connect Microphone</button>
 
 
 
 
 
 
114
  </div>
115
 
116
  <script>
117
  const connectButton = document.getElementById('connect-button');
118
  const statusDiv = document.getElementById('status');
 
 
119
 
120
- // This will be automatically provided by Livekit when you deploy
121
- // For local dev, you'll set LIVEKIT_URL in your .env
122
- const LIVEKIT_URL = `wss://${window.location.hostname}`;
123
  const TOKEN_ENDPOINT = '/get-token';
124
 
125
  let room = null;
@@ -135,49 +161,75 @@
135
 
136
  connectButton.disabled = true;
137
  connectButton.textContent = "Initializing...";
138
- updateStatus('Connecting...', 'status-connecting');
139
 
140
  try {
141
- // Fetch a token from our server
142
  const resp = await fetch(TOKEN_ENDPOINT);
143
  if (!resp.ok) {
144
- throw new Error(`Failed to fetch token: ${resp.status} ${resp.statusText}`);
145
  }
146
- const { token } = await resp.json();
 
 
 
 
 
 
147
 
148
- // Create and configure the room
149
  room = new LivekitClient.Room({
150
  audioCaptureDefaults: {
151
  autoGainControl: true,
 
152
  noiseSuppression: true,
153
  },
 
 
154
  });
155
 
156
- // Set up event listeners
157
  room.on(LivekitClient.RoomEvent.Disconnected, () => {
158
  console.log('Disconnected from room');
159
  handleDisconnect();
160
  });
 
 
 
 
 
 
 
 
 
161
 
162
- // Connect to the room
163
- await room.connect(LIVEKIT_URL, token);
164
  console.log('Successfully connected to room', room.name);
165
 
166
- // Start microphone
 
 
167
  await room.localParticipant.setMicrophoneEnabled(true);
168
  console.log('Microphone enabled');
169
 
170
  // Update UI for connected state
171
  isConnected = true;
172
- updateStatus('Connected. Start talking!', 'status-connected');
173
  connectButton.textContent = "Disconnect";
174
  connectButton.disabled = false;
175
 
176
  } catch (error) {
177
  console.error('Connection failed:', error);
178
- updateStatus(`Error: ${error.message}`, 'status-disconnected');
 
 
 
 
 
179
  connectButton.textContent = "Connect Microphone";
180
  connectButton.disabled = false;
 
181
  }
182
  }
183
 
@@ -190,6 +242,7 @@
190
  updateStatus('Disconnected', 'status-disconnected');
191
  connectButton.textContent = "Connect Microphone";
192
  connectButton.disabled = false;
 
193
  }
194
 
195
  connectButton.addEventListener('click', () => {
@@ -199,6 +252,44 @@
199
  connectToRoom();
200
  }
201
  });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
202
 
203
  </script>
204
  </body>
 
60
  padding: 10px;
61
  border-radius: 4px;
62
  transition: all 0.3s ease;
63
+ word-break: break-word; /* Prevent long errors from breaking layout */
64
  }
65
 
66
  .status-disconnected {
 
104
  color: #888;
105
  cursor: not-allowed;
106
  }
107
+
108
+ /* Visualizer bars (optional enhancement) */
109
+ #visualizer {
110
+ display: flex;
111
+ justify-content: center;
112
+ align-items: center;
113
+ height: 30px;
114
+ margin-top: 20px;
115
+ gap: 3px;
116
+ opacity: 0;
117
+ transition: opacity 0.5s;
118
+ }
119
+
120
+ .bar {
121
+ width: 4px;
122
+ background-color: var(--secondary-color);
123
+ border-radius: 2px;
124
+ height: 5px;
125
+ transition: height 0.1s ease;
126
+ }
127
  </style>
128
  </head>
129
  <body>
130
  <div class="container">
131
  <h1>DevRaze, the Roastmaster AI</h1>
132
+ <p>Embedded in Rajesh Yarra’s portfolio. Ask me anything about him. I dare you.</p>
133
+ <div id="status" class="status-disconnected">Status: Ready to roast.</div>
134
  <button id="connect-button">Connect Microphone</button>
135
+
136
+ <!-- Simple visualizer -->
137
+ <div id="visualizer">
138
+ <div class="bar"></div><div class="bar"></div><div class="bar"></div>
139
+ <div class="bar"></div><div class="bar"></div>
140
+ </div>
141
  </div>
142
 
143
  <script>
144
  const connectButton = document.getElementById('connect-button');
145
  const statusDiv = document.getElementById('status');
146
+ const visualizer = document.getElementById('visualizer');
147
+ const bars = document.querySelectorAll('.bar');
148
 
 
 
 
149
  const TOKEN_ENDPOINT = '/get-token';
150
 
151
  let room = null;
 
161
 
162
  connectButton.disabled = true;
163
  connectButton.textContent = "Initializing...";
164
+ updateStatus('Getting credentials...', 'status-connecting');
165
 
166
  try {
167
+ // 1. Fetch token and Livekit URL from our server
168
  const resp = await fetch(TOKEN_ENDPOINT);
169
  if (!resp.ok) {
170
+ throw new Error(`Server error: ${resp.statusText}`);
171
  }
172
+ const data = await resp.json();
173
+
174
+ if (!data.token || !data.livekitUrl) {
175
+ throw new Error("Server didn't return token or URL");
176
+ }
177
+
178
+ updateStatus('Connecting to Livekit...', 'status-connecting');
179
 
180
+ // 2. Create the room
181
  room = new LivekitClient.Room({
182
  audioCaptureDefaults: {
183
  autoGainControl: true,
184
+ echoCancellation: true,
185
  noiseSuppression: true,
186
  },
187
+ adaptiveStream: true,
188
+ dynacast: true,
189
  });
190
 
191
+ // Setup Room Event Listeners
192
  room.on(LivekitClient.RoomEvent.Disconnected, () => {
193
  console.log('Disconnected from room');
194
  handleDisconnect();
195
  });
196
+
197
+ // Handle incoming audio from the Agent
198
+ room.on(LivekitClient.RoomEvent.TrackSubscribed, (track, publication, participant) => {
199
+ if (track.kind === LivekitClient.Track.Kind.Audio) {
200
+ const element = track.attach();
201
+ document.body.appendChild(element);
202
+ startVisualizer(track); // Optional: start simple visualizer
203
+ }
204
+ });
205
 
206
+ // 3. Connect to Livekit Cloud using the data from server
207
+ await room.connect(data.livekitUrl, data.token);
208
  console.log('Successfully connected to room', room.name);
209
 
210
+ updateStatus('Activating microphone...', 'status-connecting');
211
+
212
+ // 4. Publish local microphone
213
  await room.localParticipant.setMicrophoneEnabled(true);
214
  console.log('Microphone enabled');
215
 
216
  // Update UI for connected state
217
  isConnected = true;
218
+ updateStatus('Connected. Prepare to be roasted.', 'status-connected');
219
  connectButton.textContent = "Disconnect";
220
  connectButton.disabled = false;
221
 
222
  } catch (error) {
223
  console.error('Connection failed:', error);
224
+ // Make error readable
225
+ let errorMsg = error.message;
226
+ if (errorMsg.includes('could not establish signal connection')) {
227
+ errorMsg = "Could not connect to Livekit Cloud. Check server logs.";
228
+ }
229
+ updateStatus(`Error: ${errorMsg}`, 'status-disconnected');
230
  connectButton.textContent = "Connect Microphone";
231
  connectButton.disabled = false;
232
+ handleDisconnect(); // Ensure cleanup
233
  }
234
  }
235
 
 
242
  updateStatus('Disconnected', 'status-disconnected');
243
  connectButton.textContent = "Connect Microphone";
244
  connectButton.disabled = false;
245
+ stopVisualizer();
246
  }
247
 
248
  connectButton.addEventListener('click', () => {
 
252
  connectToRoom();
253
  }
254
  });
255
+
256
+ // --- Simple Audio Visualizer (Optional) ---
257
+ let audioContext, analyser, dataArray, visualizerFrame;
258
+
259
+ function startVisualizer(track) {
260
+ visualizer.style.opacity = 1;
261
+ if (!audioContext) {
262
+ audioContext = new (window.AudioContext || window.webkitAudioContext)();
263
+ }
264
+ const stream = new MediaStream([track.mediaStreamTrack]);
265
+ const source = audioContext.createMediaStreamSource(stream);
266
+ analyser = audioContext.createAnalyser();
267
+ analyser.fftSize = 32;
268
+ source.connect(analyser);
269
+ dataArray = new Uint8Array(analyser.frequencyBinCount);
270
+ animateVisualizer();
271
+ }
272
+
273
+ function animateVisualizer() {
274
+ if (!isConnected || !analyser) return;
275
+ analyser.getByteFrequencyData(dataArray);
276
+
277
+ // Map frequency data to the 5 bars
278
+ const indices = [1, 3, 5, 7, 9]; // Pick some frequencies
279
+ bars.forEach((bar, i) => {
280
+ const value = dataArray[indices[i]] || 0;
281
+ const height = Math.max(5, (value / 255) * 30);
282
+ bar.style.height = `${height}px`;
283
+ });
284
+ visualizerFrame = requestAnimationFrame(animateVisualizer);
285
+ }
286
+
287
+ function stopVisualizer() {
288
+ visualizer.style.opacity = 0;
289
+ if (visualizerFrame) cancelAnimationFrame(visualizerFrame);
290
+ // Reset bars
291
+ bars.forEach(bar => bar.style.height = '5px');
292
+ }
293
 
294
  </script>
295
  </body>