Anna Sun Justin Haaheim Mark Duppenthaler commited on
Commit
2bd3674
1 Parent(s): fd69a21

Initial OSS demo commit

Browse files

---------

Co-authored-by: Justin Haaheim <justinhaaheim@users.noreply.github.com>
Co-authored-by: Mark Duppenthaler <mduppes@gmail.com>

This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. seamless_server/app_pubsub.py +790 -0
  2. seamless_server/src/connection_tracker.py +64 -0
  3. seamless_server/src/room.py +64 -0
  4. seamless_server/src/simuleval_agent_directory.py +163 -0
  5. seamless_server/src/simuleval_transcoder.py +423 -0
  6. seamless_server/src/speech_and_text_output.py +15 -0
  7. seamless_server/src/timing.ipynb +169 -0
  8. seamless_server/src/transcoder_helpers.py +43 -0
  9. streaming-react-app/.eslintrc.cjs +18 -0
  10. streaming-react-app/.gitignore +24 -0
  11. streaming-react-app/README.md +65 -0
  12. streaming-react-app/index.html +13 -0
  13. streaming-react-app/package-lock.json +0 -0
  14. streaming-react-app/package.json +58 -0
  15. streaming-react-app/public/vite.svg +1 -0
  16. streaming-react-app/src/App.tsx +57 -0
  17. streaming-react-app/src/Blink.tsx +41 -0
  18. streaming-react-app/src/DebugSection.tsx +62 -0
  19. streaming-react-app/src/RoomConfig.tsx +263 -0
  20. streaming-react-app/src/SeamlessLogo.tsx +33 -0
  21. streaming-react-app/src/SocketWrapper.tsx +217 -0
  22. streaming-react-app/src/StreamingInterface.css +66 -0
  23. streaming-react-app/src/StreamingInterface.tsx +1149 -0
  24. streaming-react-app/src/URLParams.ts +50 -0
  25. streaming-react-app/src/assets/Roboto-msdf.json +0 -0
  26. streaming-react-app/src/assets/Roboto-msdf.png +0 -0
  27. streaming-react-app/src/assets/RobotoMono-Regular-msdf.json +0 -0
  28. streaming-react-app/src/assets/RobotoMono-Regular.png +0 -0
  29. streaming-react-app/src/assets/seamless.svg +6 -0
  30. streaming-react-app/src/createBufferedSpeechPlayer.ts +177 -0
  31. streaming-react-app/src/cursorBlinkInterval.ts +1 -0
  32. streaming-react-app/src/debug.ts +257 -0
  33. streaming-react-app/src/float32To16BitPCM.ts +16 -0
  34. streaming-react-app/src/generateNewRoomID.ts +60 -0
  35. streaming-react-app/src/getParamFlag.ts +39 -0
  36. streaming-react-app/src/getTranslationSentencesFromReceivedData.ts +23 -0
  37. streaming-react-app/src/index.css +0 -0
  38. streaming-react-app/src/isScrolledToDocumentBottom.ts +11 -0
  39. streaming-react-app/src/main.tsx +10 -0
  40. streaming-react-app/src/react-xr/Button.tsx +117 -0
  41. streaming-react-app/src/react-xr/Colors.ts +6 -0
  42. streaming-react-app/src/react-xr/MovementController.tsx +64 -0
  43. streaming-react-app/src/react-xr/Playground.tsx +133 -0
  44. streaming-react-app/src/react-xr/TextBlocks.tsx +235 -0
  45. streaming-react-app/src/react-xr/ThreeMeshUIText.tsx +22 -0
  46. streaming-react-app/src/react-xr/XRConfig.tsx +449 -0
  47. streaming-react-app/src/react-xr/XRDialog.css +13 -0
  48. streaming-react-app/src/react-xr/XRDialog.tsx +49 -0
  49. streaming-react-app/src/setURLParam.ts +40 -0
  50. streaming-react-app/src/sliceTranslationSentencesUtils.ts +30 -0
seamless_server/app_pubsub.py ADDED
@@ -0,0 +1,790 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from operator import itemgetter
2
+ import os
3
+ from typing import Any, Optional, Tuple, Dict, TypedDict
4
+ from urllib import parse
5
+ from uuid import uuid4
6
+ import colorlog
7
+ import io
8
+ import logging
9
+ from pprint import pformat
10
+ import socketio
11
+ import sys
12
+ import time
13
+ import random
14
+ import string
15
+
16
+ from src.room import Room, Member
17
+ from src.simuleval_agent_directory import NoAvailableAgentException
18
+ from src.simuleval_agent_directory import SimulevalAgentDirectory
19
+ from src.simuleval_transcoder import SimulevalTranscoder
20
+ from src.transcoder_helpers import get_transcoder_output_events
21
+
22
+ ###############################################
23
+ # Constants
24
+ ###############################################
25
+
26
+ DEBUG = True
27
+
28
+ ALL_ROOM_ID = "ALL"
29
+
30
+ ROOM_ID_USABLE_CHARACTERS = string.ascii_uppercase
31
+ ROOM_ID_LENGTH = 4
32
+
33
+ ROOM_LISTENERS_SUFFIX = "_listeners"
34
+ ROOM_SPEAKERS_SUFFIX = "_speakers"
35
+
36
+ ESCAPE_HATCH_SERVER_LOCK_RELEASE_NAME = "remove_server_lock"
37
+
38
+ ###############################################
39
+ # Configure logger
40
+ ###############################################
41
+
42
+ logger = logging.getLogger("socketio_server_pubsub")
43
+ logger.propagate = False
44
+
45
+ handler = colorlog.StreamHandler(stream=sys.stdout)
46
+
47
+ formatter = colorlog.ColoredFormatter(
48
+ "%(log_color)s[%(asctime)s][%(levelname)s][%(module)s]:%(reset)s %(message)s",
49
+ reset=True,
50
+ log_colors={
51
+ "DEBUG": "cyan",
52
+ "INFO": "green",
53
+ "WARNING": "yellow",
54
+ "ERROR": "red",
55
+ "CRITICAL": "red,bg_white",
56
+ },
57
+ )
58
+
59
+ handler.setFormatter(formatter)
60
+ logger.addHandler(handler)
61
+
62
+ logger.setLevel(logging.WARNING)
63
+
64
+ print("")
65
+ print("")
66
+ print("=" * 20 + " ⭐️ Starting Server... ⭐️ " + "=" * 20)
67
+
68
+ ###############################################
69
+ # Configure socketio server
70
+ ###############################################
71
+
72
+ CLIENT_BUILD_PATH = "../streaming-react-app/dist/"
73
+ static_files = {
74
+ "/": CLIENT_BUILD_PATH,
75
+ "/assets/seamless-db6a2555.svg": {
76
+ "filename": CLIENT_BUILD_PATH + "assets/seamless-db6a2555.svg",
77
+ "content_type": "image/svg+xml",
78
+ },
79
+ }
80
+
81
+ # sio is the main socket.io entrypoint
82
+ sio = socketio.AsyncServer(
83
+ async_mode="asgi",
84
+ cors_allowed_origins="*",
85
+ logger=logger,
86
+ # engineio_logger=logger,
87
+ )
88
+ # sio.logger.setLevel(logging.DEBUG)
89
+ app = socketio.ASGIApp(sio, static_files=static_files)
90
+
91
+ # rooms is indexed by room_id
92
+ rooms: Dict[str, Room] = {}
93
+
94
+
95
+ class MemberDirectoryObject(TypedDict):
96
+ room: Room
97
+ member_object: Member
98
+
99
+
100
+ # member_directory is indexed by client_id
101
+ # NOTE: client_id is really "client session id", meaning that it is unique to a single browser session.
102
+ # If a user opens a new tab, they will have a different client_id and can join another room, join
103
+ # the same room with different roles, etc.
104
+ # NOTE: For a long-running production server we would want to clean up members after a certain timeout
105
+ # but for this limited application we can just keep them around
106
+ member_directory: Dict[str, MemberDirectoryObject] = {}
107
+
108
+
109
+ class ServerLock(TypedDict):
110
+ name: str
111
+ client_id: str
112
+ member_object: Member
113
+
114
+
115
+ server_lock: Optional[ServerLock] = None
116
+
117
+ server_id = str(uuid4())
118
+
119
+ # Specify specific models to load (some environments have issues loading multiple models)
120
+ # See AgentWithInfo with JSON format details.
121
+ models_override = os.environ.get("MODELS_OVERRIDE")
122
+
123
+ available_agents = SimulevalAgentDirectory()
124
+ logger.info("Building and adding agents...")
125
+ if models_override is not None:
126
+ logger.info(f"MODELS_OVERRIDE supplied from env vars: {models_override}")
127
+ available_agents.build_and_add_agents(models_override)
128
+
129
+ agents_capabilities_for_json = available_agents.get_agents_capabilities_list_for_json()
130
+
131
+
132
+ ###############################################
133
+ # Helpers
134
+ ###############################################
135
+
136
+
137
+ def catch_and_log_exceptions_for_sio_event_handlers(func):
138
+ # wrapper should have the same signature as the original function
139
+ async def catch_exception_wrapper(*args, **kwargs):
140
+ try:
141
+ return await func(*args, **kwargs)
142
+ except Exception as e:
143
+ message = f"[app_pubsub] Caught exception in '{func.__name__}' event handler:\n\n{e}"
144
+ logger.exception(message, stack_info=True)
145
+
146
+ try:
147
+ exception_data = {
148
+ "message": message,
149
+ "timeEpochMs": int(time.time() * 1000),
150
+ }
151
+
152
+ try:
153
+ # Let's try to add as much useful metadata as possible to the server_exception event
154
+ sid = args[0]
155
+ if isinstance(sid, str) and len(sid) > 0:
156
+ session_data = await get_session_data(sid)
157
+ if session_data:
158
+ client_id = session_data.get("client_id")
159
+ member = session_data.get("member_object")
160
+ room = session_data.get("room_object")
161
+
162
+ exception_data["room"] = str(room)
163
+ exception_data["member"] = str(member)
164
+ exception_data["clientID"] = str(client_id)
165
+ except Exception as inner_e:
166
+ # We expect there will be times when clientID or other values aren't present, so just log this as a warning
167
+ logger.warn(
168
+ f"[app_pubsub] Caught exception while trying add additional_data to server_exception:\n\n{inner_e}"
169
+ )
170
+
171
+ # For now let's emit this to all clients. We ultimatley may want to emit it just to the room it's happening in.
172
+ await sio.emit("server_exception", exception_data)
173
+ except Exception as inner_e:
174
+ logger.exception(
175
+ f"[app_pubsub] Caught exception while trying to emit server_exception event:\n{inner_e}"
176
+ )
177
+
178
+ # Re-raise the exception so it's handled normally by the server
179
+ raise e
180
+
181
+ # Set the name of the wrapper to the name of the original function so that the socketio server can associate it with the right event
182
+ catch_exception_wrapper.__name__ = func.__name__
183
+ return catch_exception_wrapper
184
+
185
+
186
+ async def emit_room_state_update(room):
187
+ await sio.emit(
188
+ "room_state_update",
189
+ room.to_json(),
190
+ room=room.room_id,
191
+ )
192
+
193
+
194
+ async def emit_server_state_update():
195
+ room_statuses = {
196
+ room_id: room.get_room_status_dict() for room_id, room in rooms.items()
197
+ }
198
+ total_active_connections = sum(
199
+ [room_status["activeConnections"] for room_status in room_statuses.values()]
200
+ )
201
+ total_active_transcoders = sum(
202
+ [room_status["activeTranscoders"] for room_status in room_statuses.values()]
203
+ )
204
+ logger.info(
205
+ f"[Server Status]: {total_active_connections} active connections (in rooms); {total_active_transcoders} active transcoders"
206
+ )
207
+ logger.info(f"[Server Status]: server_lock={server_lock}")
208
+ server_lock_object_for_js = (
209
+ {
210
+ "name": server_lock.get("name"),
211
+ "clientID": server_lock.get("client_id"),
212
+ "isActive": server_lock.get("member_object")
213
+ and server_lock.get("member_object").transcoder is not None,
214
+ }
215
+ if server_lock
216
+ else None
217
+ )
218
+ await sio.emit(
219
+ "server_state_update",
220
+ {
221
+ "statusByRoom": room_statuses,
222
+ "totalActiveConnections": total_active_connections,
223
+ "totalActiveTranscoders": total_active_transcoders,
224
+ "agentsCapabilities": agents_capabilities_for_json,
225
+ "serverLock": server_lock_object_for_js,
226
+ },
227
+ room=ALL_ROOM_ID,
228
+ )
229
+
230
+
231
+ async def get_session_data(sid):
232
+ session = await sio.get_session(sid)
233
+ # It seems like if the session has not been set that get_session may return None, so let's provide a fallback empty dictionary here
234
+ return session or {}
235
+
236
+
237
+ async def set_session_data(sid, client_id, room_id, room_object, member_object):
238
+ await sio.save_session(
239
+ sid,
240
+ {
241
+ "client_id": client_id,
242
+ "room_id": room_id,
243
+ "room_object": room_object,
244
+ "member_object": member_object,
245
+ },
246
+ )
247
+
248
+
249
+ def get_random_room_id():
250
+ return "".join(random.choices(ROOM_ID_USABLE_CHARACTERS, k=ROOM_ID_LENGTH))
251
+
252
+
253
+ def get_random_unused_room_id():
254
+ room_id = get_random_room_id()
255
+ while room_id in rooms:
256
+ room_id = get_random_room_id()
257
+ return room_id
258
+
259
+
260
+ ###############################################
261
+ # Socket.io Basic Event Handlers
262
+ ###############################################
263
+
264
+
265
+ @sio.on("connect")
266
+ @catch_and_log_exceptions_for_sio_event_handlers
267
+ async def connect(sid, environ):
268
+ logger.info(f"📥 [event: connected] sid={sid}")
269
+
270
+ # TODO: Sanitize/validate query param input
271
+ query_params = dict(parse.parse_qsl(environ["QUERY_STRING"]))
272
+ client_id = query_params.get("clientID")
273
+
274
+ logger.debug(f"query_params:\n{pformat(query_params)}")
275
+
276
+ if client_id is None:
277
+ logger.info("No clientID provided. Disconnecting...")
278
+ await sio.disconnect(sid)
279
+ return
280
+
281
+ # On reconnect we need to rejoin rooms and reset session data
282
+ if member_directory.get(client_id):
283
+ room = member_directory[client_id].get("room")
284
+ room_id = room.room_id
285
+ # Note: We could also get this from room.members[client_id]
286
+ member = member_directory[client_id].get("member_object")
287
+
288
+ member.connection_status = "connected"
289
+ member.session_id = sid
290
+
291
+ logger.info(
292
+ f"[event: connect] {member} reconnected. Attempting to re-add them to socketio rooms and reset session data."
293
+ )
294
+
295
+ if room is None or member is None:
296
+ logger.error(
297
+ f"[event: connect] {client_id} is reconnecting, but room or member is None. This should not happen."
298
+ )
299
+ await sio.disconnect(sid)
300
+ return
301
+
302
+ sio.enter_room(sid, room_id)
303
+ sio.enter_room(sid, ALL_ROOM_ID)
304
+
305
+ if client_id in room.listeners:
306
+ sio.enter_room(sid, f"{room_id}{ROOM_LISTENERS_SUFFIX}")
307
+ if client_id in room.speakers:
308
+ sio.enter_room(sid, f"{room_id}{ROOM_SPEAKERS_SUFFIX}")
309
+
310
+ # Save the room_id to the socketio client session
311
+ await set_session_data(
312
+ sid,
313
+ client_id=client_id,
314
+ room_id=room.room_id,
315
+ room_object=room,
316
+ member_object=member,
317
+ )
318
+ await emit_room_state_update(room)
319
+ else:
320
+ # Save the client id to the socketio client session
321
+ await set_session_data(
322
+ sid, client_id=client_id, room_id=None, room_object=None, member_object=None
323
+ )
324
+
325
+ await sio.emit("server_id", server_id, to=sid)
326
+ await emit_server_state_update()
327
+
328
+
329
+ @sio.event
330
+ @catch_and_log_exceptions_for_sio_event_handlers
331
+ async def disconnect(sid):
332
+ global server_lock
333
+ session_data = await get_session_data(sid)
334
+ # logger.info("session_data", session_data)
335
+
336
+ client_id = None
337
+ member = None
338
+ room = None
339
+
340
+ if session_data:
341
+ client_id = session_data.get("client_id")
342
+ member = session_data.get("member_object")
343
+ room = session_data.get("room_object")
344
+
345
+ logger.info(
346
+ f"[event: disconnect][{room or 'NOT_IN_ROOM'}] member: {member or 'NO_MEMBER_OBJECT'} disconnected"
347
+ )
348
+
349
+ # Release the lock if this is the client that holds the current server lock
350
+ if server_lock and server_lock.get("client_id") == client_id:
351
+ server_lock = None
352
+
353
+ if member:
354
+ member.connection_status = "disconnected"
355
+
356
+ if member.transcoder:
357
+ member.transcoder.close = True
358
+ member.transcoder = None
359
+ member.requested_output_type = None
360
+
361
+ if room:
362
+ logger.info(
363
+ f"[event: disconnect] {member} disconnected from room {room.room_id}"
364
+ )
365
+ await emit_room_state_update(room)
366
+ else:
367
+ logger.info(
368
+ f"[event: disconnect] {member} disconnected, but no room object present. This should not happen."
369
+ )
370
+ else:
371
+ logger.info(
372
+ f"[event: disconnect] client_id {client_id or 'NO_CLIENT_ID'} with sid {sid} in rooms {str(sio.rooms(sid))} disconnected"
373
+ )
374
+
375
+ await emit_server_state_update()
376
+
377
+
378
+ @sio.on("*")
379
+ async def catch_all(event, sid, data):
380
+ logger.info(f"[unhandled event: {event}] sid={sid} data={data}")
381
+
382
+
383
+ ###############################################
384
+ # Socket.io Streaming Event handlers
385
+ ###############################################
386
+
387
+
388
+ @sio.on("join_room")
389
+ @catch_and_log_exceptions_for_sio_event_handlers
390
+ async def join_room(sid, client_id, room_id_from_client, config_dict):
391
+ global server_lock
392
+
393
+ args = {
394
+ "sid": sid,
395
+ "client_id": client_id,
396
+ "room_id": room_id_from_client,
397
+ "config_dict": config_dict,
398
+ }
399
+ logger.info(f"[event: join_room] {args}")
400
+ session_data = await get_session_data(sid)
401
+ logger.info(f"session_data: {session_data}")
402
+
403
+ room_id = room_id_from_client
404
+ if room_id is None:
405
+ room_id = get_random_unused_room_id()
406
+ logger.info(
407
+ f"No room_id provided. Generating a random, unused room_id: {room_id}"
408
+ )
409
+
410
+ # Create the room if it doesn't already exist
411
+ if room_id not in rooms:
412
+ rooms[room_id] = Room(room_id)
413
+
414
+ room = rooms[room_id]
415
+
416
+ member = None
417
+
418
+ name = "[NO_NAME]"
419
+
420
+ # If the client is reconnecting use their existing member object. Otherwise create a new one.
421
+ if client_id in room.members:
422
+ member = room.members[client_id]
423
+ logger.info(f"{member} is rejoining room {room_id}.")
424
+ else:
425
+ member_number = len(room.members) + 1
426
+ name = f"Member {member_number}"
427
+ member = Member(
428
+ client_id=client_id,
429
+ session_id=sid,
430
+ name=name,
431
+ )
432
+ logger.info(f"Created a new Member object: {member}")
433
+ logger.info(f"Adding {member} to room {room_id}")
434
+ room.members[client_id] = member
435
+
436
+ # Also add them to the member directory
437
+ member_directory[client_id] = {"room": room, "member_object": member}
438
+
439
+ # Join the socketio room, which enables broadcasting to all members of the room
440
+ sio.enter_room(sid, room_id)
441
+ # Join the room for all clients
442
+ sio.enter_room(sid, ALL_ROOM_ID)
443
+
444
+ if "listener" in config_dict["roles"]:
445
+ sio.enter_room(sid, f"{room_id}{ROOM_LISTENERS_SUFFIX}")
446
+ if client_id not in room.listeners:
447
+ room.listeners.append(client_id)
448
+ else:
449
+ sio.leave_room(sid, f"{room_id}{ROOM_LISTENERS_SUFFIX}")
450
+ room.listeners = [
451
+ listener_id for listener_id in room.listeners if listener_id != client_id
452
+ ]
453
+
454
+ if "speaker" in config_dict["roles"]:
455
+ sio.enter_room(sid, f"{room_id}{ROOM_SPEAKERS_SUFFIX}")
456
+ if client_id not in room.speakers:
457
+ room.speakers.append(client_id)
458
+ else:
459
+ sio.leave_room(sid, f"{room_id}{ROOM_SPEAKERS_SUFFIX}")
460
+ # If the person is no longer a speaker they should no longer be able to lock the server
461
+ if server_lock and server_lock.get("client_id") == client_id:
462
+ logger.info(
463
+ f"🔓 Server is now unlocked from client {server_lock.get('client_id')} with name/info: {server_lock.get('name')}"
464
+ )
465
+ server_lock = None
466
+ if member.transcoder:
467
+ member.transcoder.close = True
468
+ member.transcoder = None
469
+ room.speakers = [
470
+ speaker_id for speaker_id in room.speakers if speaker_id != client_id
471
+ ]
472
+
473
+ # If we currently own the server lock and are updating roles and we no longer have server lock specified, release it
474
+ if (
475
+ server_lock is not None
476
+ and server_lock["client_id"] == client_id
477
+ and config_dict.get("lockServerName") is None
478
+ ):
479
+ logger.info(f"[join_room] Releasing server lock: {pformat(server_lock)}")
480
+ server_lock = None
481
+
482
+ # Only speakers should be able to lock the server
483
+ if config_dict.get("lockServerName") is not None and "speaker" in config_dict.get(
484
+ "roles", {}
485
+ ):
486
+ # If something goes wrong and the server gets stuck in a locked state the client can
487
+ # force the server to remove the lock by passing the special name ESCAPE_HATCH_SERVER_LOCK_RELEASE_NAME
488
+ if (
489
+ server_lock is not None
490
+ and config_dict.get("lockServerName")
491
+ == ESCAPE_HATCH_SERVER_LOCK_RELEASE_NAME
492
+ ):
493
+ server_lock = None
494
+ logger.info(
495
+ f"🔓 Server lock has been reset by {client_id} using the escape hatch name {ESCAPE_HATCH_SERVER_LOCK_RELEASE_NAME}"
496
+ )
497
+
498
+ # If the server is not locked, set a lock. If it's already locked to this client, update the lock object
499
+ elif server_lock is None or server_lock.get("client_id") == client_id:
500
+ # TODO: Add some sort of timeout as a backstop in case someone leaves the browser tab open after locking the server
501
+ server_lock = {
502
+ "name": config_dict.get("lockServerName"),
503
+ "client_id": client_id,
504
+ "member_object": member,
505
+ }
506
+ logger.info(
507
+ f"🔒 Server is now locked to client {server_lock.get('client_id')} with name/info: {server_lock.get('name')}\nThis client will have priority over all others until they disconnect."
508
+ )
509
+ # If the server is already locked to someone else, don't allow this client to lock it
510
+ elif server_lock is not None and server_lock.get("client_id") != client_id:
511
+ logger.warn(
512
+ f"⚠️ Server is already locked to client {server_lock.get('client_id')}. Ignoring request to lock to client {client_id}."
513
+ )
514
+ # TODO: Maybe throw an error here?
515
+
516
+ # Save the room_id to the socketio client session
517
+ await set_session_data(
518
+ sid,
519
+ client_id=client_id,
520
+ room_id=room_id,
521
+ room_object=room,
522
+ member_object=member,
523
+ )
524
+
525
+ await emit_room_state_update(room)
526
+ await emit_server_state_update()
527
+
528
+ return {"roomsJoined": sio.rooms(sid), "roomID": room_id}
529
+
530
+
531
+ # TODO: Add code to prevent more than one speaker from connecting/streaming at a time
532
+ @sio.event
533
+ @catch_and_log_exceptions_for_sio_event_handlers
534
+ async def configure_stream(sid, config):
535
+ session_data = await get_session_data(sid)
536
+ client_id, member, room = itemgetter("client_id", "member_object", "room_object")(
537
+ session_data
538
+ )
539
+
540
+ logger.debug(
541
+ f"[event: configure_stream][{room}] Received stream config from {member}\n{pformat(config)}"
542
+ )
543
+
544
+ if member is None or room is None:
545
+ logger.error(
546
+ f"Received stream config from {member}, but member or room is None. This should not happen."
547
+ )
548
+ return {"status": "error", "message": "member_or_room_is_none"}
549
+
550
+ # If there is a server lock WITH an active transcoder session, prevent other users from configuring and starting a stream
551
+ # If the server lock client does NOT have an active transcoder session allow this to proceed, knowing that
552
+ # this stream will be interrupted if the server lock client starts streaming
553
+ if (
554
+ server_lock is not None
555
+ and server_lock.get("client_id") != client_id
556
+ and server_lock.get("member_object")
557
+ and server_lock.get("member_object").transcoder is not None
558
+ ):
559
+ logger.warn(
560
+ f"Server is locked to client {server_lock.get('client_id')}. Ignoring request to configure stream from client {client_id}."
561
+ )
562
+ return {"status": "error", "message": "server_locked"}
563
+
564
+ debug = config.get("debug")
565
+ async_processing = config.get("async_processing")
566
+
567
+ # Currently s2s, s2t or s2s&t
568
+ model_type = config.get("model_type")
569
+ member.requested_output_type = model_type
570
+
571
+ model_name = config.get("model_name")
572
+
573
+ try:
574
+ agent = available_agents.get_agent_or_throw(model_name)
575
+ except NoAvailableAgentException as e:
576
+ logger.warn(f"Error while getting agent: {e}")
577
+ # await sio.emit("error", str(e), to=sid)
578
+ await sio.disconnect(sid)
579
+ return {"status": "error", "message": str(e)}
580
+
581
+ if member.transcoder:
582
+ logger.warn(
583
+ "Member already has a transcoder configured. Closing it, and overwriting with a new transcoder..."
584
+ )
585
+ member.transcoder.close = True
586
+
587
+ t0 = time.time()
588
+ try:
589
+ member.transcoder = SimulevalTranscoder(
590
+ agent,
591
+ config["rate"],
592
+ debug=debug,
593
+ buffer_limit=int(config["buffer_limit"]),
594
+ )
595
+ except Exception as e:
596
+ logger.warn(f"Got exception while initializing agents: {e}")
597
+ # await sio.emit("error", str(e), to=sid)
598
+ await sio.disconnect(sid)
599
+ return {"status": "error", "message": str(e)}
600
+
601
+ t1 = time.time()
602
+ logger.debug(f"Booting up VAD and transcoder took {t1-t0} sec")
603
+
604
+ # TODO: if async_processing is false, then we need to run transcoder.process_pipeline_once() whenever we receive audio, or at some other sensible interval
605
+ if async_processing:
606
+ member.transcoder.start()
607
+
608
+ # We need to emit a room state update here since room state now includes # of active transcoders
609
+ await emit_room_state_update(room)
610
+ await emit_server_state_update()
611
+
612
+ return {"status": "ok", "message": "server_ready"}
613
+ # await sio.emit("server_ready", None, to=sid)
614
+
615
+
616
+ # The config here is a partial config, meaning it may not contain all the config values -- only the ones the user
617
+ # wants to change
618
+ @sio.on("set_dynamic_config")
619
+ @catch_and_log_exceptions_for_sio_event_handlers
620
+ async def set_dynamic_config(
621
+ sid,
622
+ # partial_config's type is defined in StreamingTypes.ts
623
+ partial_config,
624
+ ):
625
+ session_data = await get_session_data(sid)
626
+
627
+ # client_id = None
628
+ member = None
629
+ # room = None
630
+
631
+ if session_data:
632
+ # client_id = session_data.get("client_id")
633
+ member = session_data.get("member_object")
634
+ # room = session_data.get("room_object")
635
+
636
+ if member:
637
+ new_dynamic_config = {
638
+ **(member.transcoder_dynamic_config or {}),
639
+ **partial_config,
640
+ }
641
+ logger.info(
642
+ f"[set_dynamic_config] Setting new dynamic config:\n\n{pformat(new_dynamic_config)}\n"
643
+ )
644
+ member.transcoder_dynamic_config = new_dynamic_config
645
+
646
+ return {"status": "ok", "message": "dynamic_config_set"}
647
+
648
+
649
+ @sio.event
650
+ @catch_and_log_exceptions_for_sio_event_handlers
651
+ async def incoming_audio(sid, blob):
652
+ # logger.info(f"[event: incoming_audio] {sid}")
653
+
654
+ session_data = await get_session_data(sid)
655
+
656
+ client_id = None
657
+ member = None
658
+ room = None
659
+
660
+ if session_data:
661
+ client_id = session_data.get("client_id")
662
+ member = session_data.get("member_object")
663
+ room = session_data.get("room_object")
664
+
665
+ logger.debug(f"[event: incoming_audio] from member {member}")
666
+
667
+ # If the server is locked by someone else, kill our transcoder and ignore incoming audio
668
+ # If the server lock client does NOT have an active transcoder session allow this incoming audio pipeline to proceed,
669
+ # knowing that this stream will be interrupted if the server lock client starts streaming
670
+ if (
671
+ server_lock is not None
672
+ and server_lock.get("client_id") != client_id
673
+ and server_lock.get("member_object")
674
+ and server_lock.get("member_object").transcoder is not None
675
+ ):
676
+ # TODO: Send an event to the client to let them know their streaming session has been killed
677
+ if member.transcoder:
678
+ member.transcoder.close = True
679
+ member.transcoder = None
680
+ # Update both room state and server state given that the number of active transcoders has changed
681
+ if room:
682
+ await emit_room_state_update(room)
683
+ await emit_server_state_update()
684
+ logger.warn(
685
+ f"[incoming_audio] Server is locked to client {server_lock.get('client_id')}. Ignoring incoming audio from client {client_id}."
686
+ )
687
+ return
688
+
689
+ if member is None or room is None:
690
+ logger.error(
691
+ f"[incoming_audio] Received incoming_audio from {member}, but member or room is None. This should not happen."
692
+ )
693
+ return
694
+
695
+ # NOTE: bytes and bytearray are very similar, but bytes is immutable, and is what is returned by socketio
696
+ if not isinstance(blob, bytes):
697
+ logger.error(
698
+ f"[incoming_audio] Received audio from {member}, but it was not of type `bytes`. type(blob) = {type(blob)}"
699
+ )
700
+ return
701
+
702
+ if member.transcoder is None:
703
+ logger.error(
704
+ f"[incoming_audio] Received audio from {member}, but no transcoder configured to process it (member.transcoder is None). This should not happen."
705
+ )
706
+ return
707
+
708
+ member.transcoder.process_incoming_bytes(
709
+ blob, dynamic_config=member.transcoder_dynamic_config
710
+ )
711
+
712
+ # TODO: What we have below is NOT a good way to do this, because instead of sending output when its ready we're
713
+ # sending it when new input comes in, which means it's just sitting around. This is a temporary hack until
714
+ # we figure out a better architecture for awaiting transcoder output and sending it to the client.
715
+ events = get_transcoder_output_events(member.transcoder)
716
+ logger.debug(f"[incoming_audio] transcoder output events: {len(events)}")
717
+
718
+ if len(events) == 0:
719
+ logger.debug("[incoming_audio] No transcoder output to send")
720
+ else:
721
+ for e in events:
722
+ if e["event"] == "translation_speech" and member.requested_output_type in [
723
+ "s2s",
724
+ "s2s&t",
725
+ ]:
726
+ logger.debug("[incoming_audio] Sending translation_speech event")
727
+ await sio.emit(
728
+ "translation_speech", e, room=f"{room.room_id}_listeners"
729
+ )
730
+ elif e["event"] == "translation_text" and member.requested_output_type in [
731
+ "s2t",
732
+ "s2s&t",
733
+ ]:
734
+ logger.debug("[incoming_audio] Sending translation_text event")
735
+ await sio.emit("translation_text", e, room=f"{room.room_id}_listeners")
736
+ else:
737
+ logger.error(f"[incoming_audio] Unexpected event type: {e['event']}")
738
+
739
+ return
740
+
741
+
742
+ @sio.event
743
+ @catch_and_log_exceptions_for_sio_event_handlers
744
+ async def stop_stream(sid):
745
+ session_data = await get_session_data(sid)
746
+ client_id, member, room = itemgetter("client_id", "member_object", "room_object")(
747
+ session_data
748
+ )
749
+
750
+ logger.debug(f"[event: stop_stream][{room}] Attempting to stop stream for {member}")
751
+
752
+ if member is None or room is None:
753
+ message = f"Received stop_stream from {member}, but member or room is None. This should not happen."
754
+ logger.error(message)
755
+ return {"status": "error", "message": message}
756
+
757
+ # In order to stop the stream and end the transcoder thread, set close to True and unset it for the member
758
+ if member.transcoder:
759
+ member.transcoder.close = True
760
+ member.transcoder = None
761
+ else:
762
+ message = f"Received stop_stream from {member}, but member.transcoder is None. This should not happen."
763
+ logger.warn(message)
764
+
765
+ # We need to emit a room state update here since room state now includes # of active transcoders
766
+ await emit_room_state_update(room)
767
+ # Emit a server state update now that we've changed the number of active transcoders
768
+ await emit_server_state_update()
769
+
770
+ return {"status": "ok", "message": "Stream stopped"}
771
+
772
+
773
+ @sio.on("clear_transcript_for_all")
774
+ @catch_and_log_exceptions_for_sio_event_handlers
775
+ async def clear_transcript_for_all(sid):
776
+ session_data = await get_session_data(sid)
777
+
778
+ room = session_data.get("room_object")
779
+
780
+ if room:
781
+ await sio.emit("clear_transcript", room=f"{room.room_id}")
782
+ else:
783
+ logger.error("[clear_transcript] room is None. This should not happen.")
784
+
785
+
786
+ @sio.event
787
+ @catch_and_log_exceptions_for_sio_event_handlers
788
+ async def set_name(sid, name):
789
+ logger.info(f"[Event: set_name] name={name}")
790
+ await sio.save_session(sid, {"name": name})
seamless_server/src/connection_tracker.py ADDED
@@ -0,0 +1,64 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from logging import Logger
2
+ import time
3
+
4
+
5
+ class StreamingConnectionInfo:
6
+ def __init__(self, address, active_connections, latest_message_received_timestamp):
7
+ self.address = address
8
+ self.active_connections = active_connections
9
+ self.latest_message_received_timestamp = latest_message_received_timestamp
10
+
11
+ def __repr__(self):
12
+ return str(self)
13
+
14
+ def __str__(self):
15
+ return str(
16
+ {
17
+ "address": self.address,
18
+ "active_connections": self.active_connections,
19
+ "latest_message_received_timestamp": self.latest_message_received_timestamp,
20
+ }
21
+ )
22
+
23
+
24
+ class ConnectionTracker:
25
+ def __init__(self, logger: Logger):
26
+ self.connections = dict()
27
+ self.logger = logger
28
+
29
+ def __str__(self):
30
+ return str(self.connections)
31
+
32
+ def add_connection(self, address):
33
+ if address not in self.connections:
34
+ self.connections[address] = StreamingConnectionInfo(address, 1, time.time())
35
+ else:
36
+ self.connections[address].active_connections += 1
37
+ self.connections[address].latest_message_received_timestamp = time.time()
38
+
39
+ def log_recent_message(self, address):
40
+ if address in self.connections:
41
+ self.connections[address].latest_message_received_timestamp = time.time()
42
+ else:
43
+ self.logger.warning(
44
+ f"Address {address} not found in connection tracker when attempting to log recent message"
45
+ )
46
+
47
+ def remove_connection(self, address):
48
+ if address in self.connections:
49
+ self.connections[address].active_connections -= 1
50
+ if self.connections[address].active_connections < 0:
51
+ self.logger.warning(
52
+ f"Address {address} has negative active connections ({self.connections[address].active_connections})"
53
+ )
54
+ if self.connections[address].active_connections <= 0:
55
+ del self.connections[address]
56
+ else:
57
+ self.logger.warning(
58
+ f"Address {address} not found in connection tracker when attempting to remove it"
59
+ )
60
+
61
+ def get_active_connection_count(self):
62
+ return sum(
63
+ [connection.active_connections for connection in self.connections.values()]
64
+ )
seamless_server/src/room.py ADDED
@@ -0,0 +1,64 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # import json
2
+ import uuid
3
+
4
+
5
+ class Room:
6
+ def __init__(self, room_id) -> None:
7
+ self.room_id = room_id
8
+ # members is a dict from client_id to Member
9
+ self.members = {}
10
+
11
+ # listeners and speakers are lists of client_id's
12
+ self.listeners = []
13
+ self.speakers = []
14
+
15
+ def __str__(self) -> str:
16
+ return f"Room {self.room_id} ({len(self.members)} member{'s' if len(self.members) == 1 else ''})"
17
+
18
+ def to_json(self):
19
+ varsResult = vars(self)
20
+ # Remember: result is just a shallow copy, so result.members === self.members
21
+ # Because of that, we need to jsonify self.members without writing over result.members,
22
+ # which we do here via dictionary unpacking (the ** operator)
23
+ result = {
24
+ **varsResult,
25
+ "members": {key: value.to_json() for (key, value) in self.members.items()},
26
+ "activeTranscoders": self.get_active_transcoders(),
27
+ }
28
+
29
+ return result
30
+
31
+ def get_active_connections(self):
32
+ return len(
33
+ [m for m in self.members.values() if m.connection_status == "connected"]
34
+ )
35
+
36
+ def get_active_transcoders(self):
37
+ return len([m for m in self.members.values() if m.transcoder is not None])
38
+
39
+ def get_room_status_dict(self):
40
+ return {
41
+ "activeConnections": self.get_active_connections(),
42
+ "activeTranscoders": self.get_active_transcoders(),
43
+ }
44
+
45
+
46
+ class Member:
47
+ def __init__(self, client_id, session_id, name) -> None:
48
+ self.client_id = client_id
49
+ self.session_id = session_id
50
+ self.name = name
51
+ self.connection_status = "connected"
52
+ self.transcoder = None
53
+ self.requested_output_type = None
54
+ self.transcoder_dynamic_config = None
55
+
56
+ def __str__(self) -> str:
57
+ return f"{self.name} (id: {self.client_id[:4]}...) ({self.connection_status})"
58
+
59
+ def to_json(self):
60
+ self_vars = vars(self)
61
+ return {
62
+ **self_vars,
63
+ "transcoder": self.transcoder is not None,
64
+ }
seamless_server/src/simuleval_agent_directory.py ADDED
@@ -0,0 +1,163 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Creates a directory in which to look up available agents
2
+
3
+ from typing import List
4
+ from src.simuleval_transcoder import SimulevalTranscoder
5
+ import json
6
+ import logging
7
+
8
+ logger = logging.getLogger("socketio_server_pubsub")
9
+
10
+ # fmt: off
11
+ M4T_P0_LANGS = [
12
+ "eng",
13
+ "arb", "ben", "cmn", "deu", "fil",
14
+ "fra", "hin", "ind", "ita", "jpn",
15
+ "kor", "nld", "por", "rus", "spa",
16
+ "swh", "tha", "tur", "urd", "vie",
17
+ ]
18
+ # fmt: on
19
+
20
+
21
+ class NoAvailableAgentException(Exception):
22
+ pass
23
+
24
+
25
+ class AgentWithInfo:
26
+ def __init__(
27
+ self,
28
+ agent,
29
+ name: str,
30
+ modalities: List[str],
31
+ source_langs: List[str],
32
+ target_langs: List[str],
33
+ # Supported dynamic params are defined in StreamingTypes.ts
34
+ dynamic_params: List[str] = [],
35
+ description="",
36
+ ):
37
+ self.agent = agent
38
+ self.name = name
39
+ self.description = description
40
+ self.modalities = modalities
41
+ self.source_langs = source_langs
42
+ self.target_langs = target_langs
43
+ self.dynamic_params = dynamic_params
44
+
45
+ def get_capabilities_for_json(self):
46
+ return {
47
+ "name": self.name,
48
+ "description": self.description,
49
+ "modalities": self.modalities,
50
+ "sourceLangs": self.source_langs,
51
+ "targetLangs": self.target_langs,
52
+ "dynamicParams": self.dynamic_params,
53
+ }
54
+
55
+ @classmethod
56
+ def load_from_json(cls, config: str):
57
+ """
58
+ Takes in JSON array of models to load in, e.g.
59
+ [{"name": "s2s_m4t_emma-unity2_multidomain_v0.1", "description": "M4T model that supports simultaneous S2S and S2T", "modalities": ["s2t", "s2s"], "sourceLangs": ["arb", "ben", "hin", "ind", "ita", "jpn", "por", "rus", "spa", "swh", "tha", "tur", "urd", "vie"], "targetLangs": ["en"]},
60
+ {"name": "s2s_m4t_expr-emma_v0.1", "description": "ES-EN expressive model that supports S2S and S2T", "modalities": ["s2t", "s2s"], "sourceLangs": ["arb", "ben", "hin", "ind", "ita", "jpn", "por", "rus", "spa", "swh", "tha", "tur", "urd", "vie"], "targetLangs": ["en"]}]
61
+ """
62
+ configs = json.loads(config)
63
+ agents = []
64
+ for config in configs:
65
+ agent = SimulevalTranscoder.build_agent(config["name"])
66
+ agents.append(
67
+ AgentWithInfo(
68
+ agent=agent,
69
+ name=config["name"],
70
+ modalities=config["modalities"],
71
+ source_langs=config["sourceLangs"],
72
+ target_langs=config["targetLangs"],
73
+ )
74
+ )
75
+ return agents
76
+
77
+
78
+ class SimulevalAgentDirectory:
79
+ # Available models. These are the directories where the models can be found, and also serve as an ID for the model.
80
+ # s2t:
81
+ s2t_es_en_agent = "s2t_es-en_tt-waitk_multidomain"
82
+ s2t_en_es_agent = "s2t_en-es_tt-waitk_multidomain"
83
+ s2t_es_en_emma_agent = "s2t_es-en_emma_multidomain_v0.3"
84
+ s2t_en_es_emma_agent = "s2t_en-es_emma_multidomain_v0.3"
85
+ # s2s:
86
+ s2s_es_en_agent = "s2s_es-en_tt-waitk-unity2_multidomain"
87
+ s2s_es_en_emma_agent = "s2s_es-en_emma-unity2_multidomain_v0.2"
88
+ s2s_m4t_expr_emma_agent = "s2s_m4t_expr-emma_v0.3"
89
+ s2s_m4t_emma_agent = "s2s_m4t_emma-unity2_multidomain_v0.4"
90
+
91
+ def __init__(self):
92
+ self.agents = []
93
+ self.did_build_and_add_agents = False
94
+
95
+ def add_agent(self, agent: AgentWithInfo):
96
+ self.agents.append(agent)
97
+
98
+ def build_agent_if_available(self, model_id, config_name=None):
99
+ agent = None
100
+ try:
101
+ if config_name is not None:
102
+ agent = SimulevalTranscoder.build_agent(
103
+ model_id,
104
+ config_name=config_name,
105
+ )
106
+ else:
107
+ agent = SimulevalTranscoder.build_agent(
108
+ model_id,
109
+ )
110
+ except Exception as e:
111
+ logger.warning("Failed to build agent %s: %s" % (model_id, e))
112
+ raise e
113
+
114
+ return agent
115
+
116
+ def build_and_add_agents(self, models_override=None):
117
+ if self.did_build_and_add_agents:
118
+ return
119
+
120
+ if models_override is not None:
121
+ agent_infos = AgentWithInfo.load_from_json(models_override)
122
+ for agent_info in agent_infos:
123
+ self.add_agent(agent_info)
124
+ else:
125
+ s2s_m4t_expr_agent = self.build_agent_if_available(
126
+ SimulevalAgentDirectory.s2s_m4t_emma_agent,
127
+ config_name="vad_s2st_sc_24khz_main.yaml",
128
+ )
129
+
130
+ if s2s_m4t_expr_agent:
131
+ self.add_agent(
132
+ AgentWithInfo(
133
+ agent=s2s_m4t_expr_agent,
134
+ name=SimulevalAgentDirectory.s2s_m4t_emma_agent,
135
+ modalities=["s2t", "s2s"],
136
+ source_langs=M4T_P0_LANGS,
137
+ target_langs=["eng", "spa", "fra", "deu", "ita", "cmn"],
138
+ dynamic_params=["expressive"],
139
+ description="ES-EN expressive model that supports S2S and S2T",
140
+ )
141
+ )
142
+
143
+ if len(self.agents) == 0:
144
+ logger.error(
145
+ "No agents were loaded. This likely means you are missing the actual model files specified in simuleval_agent_directory."
146
+ )
147
+
148
+ self.did_build_and_add_agents = True
149
+
150
+ def get_agent(self, name):
151
+ for agent in self.agents:
152
+ if agent.name == name:
153
+ return agent.agent
154
+ return None
155
+
156
+ def get_agent_or_throw(self, name):
157
+ agent = self.get_agent(name)
158
+ if agent is None:
159
+ raise NoAvailableAgentException("No agent found with name= %s" % (name))
160
+ return agent
161
+
162
+ def get_agents_capabilities_list_for_json(self):
163
+ return [agent.get_capabilities_for_json() for agent in self.agents]
seamless_server/src/simuleval_transcoder.py ADDED
@@ -0,0 +1,423 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from simuleval.utils.agent import build_system_from_dir
2
+ from typing import Any, List, Optional, Tuple, Union
3
+ import numpy as np
4
+ import soundfile
5
+ import io
6
+ import asyncio
7
+ from simuleval.agents.pipeline import TreeAgentPipeline
8
+ from simuleval.agents.states import AgentStates
9
+ from simuleval.data.segments import Segment, EmptySegment, SpeechSegment
10
+ import threading
11
+ import math
12
+ import logging
13
+ import sys
14
+ from pathlib import Path
15
+ import time
16
+ from g2p_en import G2p
17
+ import torch
18
+ import traceback
19
+ import time
20
+ import random
21
+ import colorlog
22
+
23
+ from .speech_and_text_output import SpeechAndTextOutput
24
+
25
+ MODEL_SAMPLE_RATE = 16_000
26
+
27
+ logger = logging.getLogger(__name__)
28
+ # logger.propagate = False
29
+ handler = colorlog.StreamHandler(stream=sys.stdout)
30
+ formatter = colorlog.ColoredFormatter(
31
+ "%(log_color)s[%(asctime)s][%(levelname)s][%(module)s]:%(reset)s %(message)s",
32
+ reset=True,
33
+ log_colors={
34
+ "DEBUG": "cyan",
35
+ "INFO": "green",
36
+ "WARNING": "yellow",
37
+ "ERROR": "red",
38
+ "CRITICAL": "red,bg_white",
39
+ },
40
+ )
41
+ handler.setFormatter(formatter)
42
+ logger.addHandler(handler)
43
+ logger.setLevel(logging.WARNING)
44
+
45
+
46
+ class OutputSegments:
47
+ def __init__(self, segments: Union[List[Segment], Segment]):
48
+ if isinstance(segments, Segment):
49
+ segments = [segments]
50
+ self.segments: List[Segment] = [s for s in segments]
51
+
52
+ @property
53
+ def is_empty(self):
54
+ return all(segment.is_empty for segment in self.segments)
55
+
56
+ @property
57
+ def finished(self):
58
+ return all(segment.finished for segment in self.segments)
59
+
60
+ def compute_length(self, g2p):
61
+ lengths = []
62
+ for segment in self.segments:
63
+ if segment.data_type == "text":
64
+ lengths.append(len([x for x in g2p(segment.content) if x != " "]))
65
+ elif segment.data_type == "speech":
66
+ lengths.append(len(segment.content) / MODEL_SAMPLE_RATE)
67
+ elif isinstance(segment, EmptySegment):
68
+ continue
69
+ else:
70
+ logger.warning(
71
+ f"Unexpected data_type: {segment.data_type} not in 'speech', 'text'"
72
+ )
73
+ return max(lengths)
74
+
75
+ @classmethod
76
+ def join_output_buffer(
77
+ cls, buffer: List[List[Segment]], output: SpeechAndTextOutput
78
+ ):
79
+ num_segments = len(buffer[0])
80
+ for i in range(num_segments):
81
+ segment_list = [
82
+ buffer[j][i]
83
+ for j in range(len(buffer))
84
+ if buffer[j][i].data_type is not None
85
+ ]
86
+ if len(segment_list) == 0:
87
+ continue
88
+ if len(set(segment.data_type for segment in segment_list)) != 1:
89
+ logger.warning(
90
+ f"Data type mismatch at {i}: {set(segment.data_type for segment in segment_list)}"
91
+ )
92
+ continue
93
+ data_type = segment_list[0].data_type
94
+ if data_type == "text":
95
+ if output.text is not None:
96
+ logger.warning("Multiple text outputs, overwriting!")
97
+ output.text = " ".join([segment.content for segment in segment_list])
98
+ elif data_type == "speech":
99
+ if output.speech_samples is not None:
100
+ logger.warning("Multiple speech outputs, overwriting!")
101
+ speech_out = []
102
+ for segment in segment_list:
103
+ speech_out += segment.content
104
+ output.speech_samples = speech_out
105
+ output.speech_sample_rate = segment.sample_rate
106
+ elif isinstance(segment_list[0], EmptySegment):
107
+ continue
108
+ else:
109
+ logger.warning(
110
+ f"Invalid output buffer data type: {data_type}, expected 'speech' or 'text"
111
+ )
112
+
113
+ return output
114
+
115
+ def __repr__(self) -> str:
116
+ repr_str = str(self.segments)
117
+ return f"{self.__class__.__name__}(\n\t{repr_str}\n)"
118
+
119
+
120
+ class SimulevalTranscoder:
121
+ def __init__(self, agent, sample_rate, debug, buffer_limit):
122
+ self.agent = agent
123
+ self.input_queue = asyncio.Queue()
124
+ self.output_queue = asyncio.Queue()
125
+ self.states = self.agent.build_states()
126
+ if debug:
127
+ self.get_states_root().debug = True
128
+ self.incoming_sample_rate = sample_rate
129
+ self.close = False
130
+ self.g2p = G2p()
131
+
132
+ # buffer all outgoing translations within this amount of time
133
+ self.output_buffer_idle_ms = 5000
134
+ self.output_buffer_size_limit = (
135
+ buffer_limit # phonemes for text, seconds for speech
136
+ )
137
+ self.output_buffer_cur_size = 0
138
+ self.output_buffer: List[List[Segment]] = []
139
+ self.speech_output_sample_rate = None
140
+
141
+ self.last_output_ts = time.time() * 1000
142
+ self.timeout_ms = (
143
+ 30000 # close the transcoder thread after this amount of silence
144
+ )
145
+ self.first_input_ts = None
146
+ self.first_output_ts = None
147
+ self.debug = debug
148
+ self.debug_ts = f"{time.time()}_{random.randint(1000, 9999)}"
149
+ if self.debug:
150
+ debug_folder = Path(__file__).resolve().parent.parent / "debug"
151
+ self.test_incoming_wav = soundfile.SoundFile(
152
+ debug_folder / f"{self.debug_ts}_test_incoming.wav",
153
+ mode="w+",
154
+ format="WAV",
155
+ subtype="PCM_16",
156
+ samplerate=self.incoming_sample_rate,
157
+ channels=1,
158
+ )
159
+ self.get_states_root().test_input_segments_wav = soundfile.SoundFile(
160
+ debug_folder / f"{self.debug_ts}_test_input_segments.wav",
161
+ mode="w+",
162
+ format="WAV",
163
+ samplerate=MODEL_SAMPLE_RATE,
164
+ channels=1,
165
+ )
166
+
167
+ def get_states_root(self) -> AgentStates:
168
+ if isinstance(self.agent, TreeAgentPipeline):
169
+ # self.states is a dict
170
+ return self.states[self.agent.source_module]
171
+ else:
172
+ # self.states is a list
173
+ return self.states[0]
174
+
175
+ def reset_states(self):
176
+ if isinstance(self.agent, TreeAgentPipeline):
177
+ states_iter = self.states.values()
178
+ else:
179
+ states_iter = self.states
180
+ for state in states_iter:
181
+ state.reset()
182
+
183
+ def debug_log(self, *args):
184
+ if self.debug:
185
+ logger.info(*args)
186
+
187
+ @classmethod
188
+ def build_agent(cls, model_path, config_name="vad_s2st_main.yaml"):
189
+ logger.info(f"Building simuleval agent: {model_path}, {config_name}")
190
+ agent = build_system_from_dir(
191
+ Path(__file__).resolve().parent.parent / f"models/{model_path}",
192
+ config_name=config_name,
193
+ )
194
+ device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
195
+ agent.to(device, fp16=True)
196
+ logger.info(
197
+ f"Successfully built simuleval agent {model_path} on device {device}"
198
+ )
199
+
200
+ return agent
201
+
202
+ def process_incoming_bytes(self, incoming_bytes, dynamic_config):
203
+ # TODO: We probably want to do some validation on dynamic_config to ensure it has what we needs
204
+ segment, sr = self._preprocess_wav(incoming_bytes)
205
+ segment = SpeechSegment(
206
+ content=segment,
207
+ sample_rate=sr,
208
+ tgt_lang=dynamic_config.get("targetLanguage"),
209
+ config=dynamic_config,
210
+ )
211
+ # # segment is array([0, 0, 0, ..., 0, 0, 0], dtype=int16)
212
+ self.input_queue.put_nowait(segment)
213
+
214
+ def get_input_segment(self):
215
+ if self.input_queue.empty():
216
+ return None
217
+ chunk = self.input_queue.get_nowait()
218
+ self.input_queue.task_done()
219
+ return chunk
220
+
221
+ def convert_waveform(
222
+ self,
223
+ waveform: Union[np.ndarray, torch.Tensor],
224
+ sample_rate: int,
225
+ normalize_volume: bool = False,
226
+ to_mono: bool = False,
227
+ to_sample_rate: Optional[int] = None,
228
+ ) -> Tuple[Union[np.ndarray, torch.Tensor], int]:
229
+ """convert a waveform:
230
+ - to a target sample rate
231
+ - from multi-channel to mono channel
232
+ - volume normalization
233
+
234
+ Args:
235
+ waveform (numpy.ndarray or torch.Tensor): 2D original waveform
236
+ (channels x length)
237
+ sample_rate (int): original sample rate
238
+ normalize_volume (bool): perform volume normalization
239
+ to_mono (bool): convert to mono channel if having multiple channels
240
+ to_sample_rate (Optional[int]): target sample rate
241
+ Returns:
242
+ waveform (numpy.ndarray): converted 2D waveform (channels x length)
243
+ sample_rate (float): target sample rate
244
+ """
245
+ try:
246
+ import torchaudio.sox_effects as ta_sox
247
+ except ImportError:
248
+ raise ImportError("Please install torchaudio: pip install torchaudio")
249
+
250
+ effects = []
251
+ if normalize_volume:
252
+ effects.append(["gain", "-n"])
253
+ if to_sample_rate is not None and to_sample_rate != sample_rate:
254
+ effects.append(["rate", f"{to_sample_rate}"])
255
+ if to_mono and waveform.shape[0] > 1:
256
+ effects.append(["channels", "1"])
257
+ if len(effects) > 0:
258
+ is_np_input = isinstance(waveform, np.ndarray)
259
+ _waveform = torch.from_numpy(waveform) if is_np_input else waveform
260
+ converted, converted_sample_rate = ta_sox.apply_effects_tensor(
261
+ _waveform, sample_rate, effects
262
+ )
263
+ if is_np_input:
264
+ converted = converted.numpy()
265
+ return converted, converted_sample_rate
266
+ return waveform, sample_rate
267
+
268
+ def _preprocess_wav(self, data: Any) -> Tuple[np.ndarray, int]:
269
+ segment, sample_rate = soundfile.read(
270
+ io.BytesIO(data),
271
+ dtype="float32",
272
+ always_2d=True,
273
+ frames=-1,
274
+ start=0,
275
+ format="RAW",
276
+ subtype="PCM_16",
277
+ samplerate=self.incoming_sample_rate,
278
+ channels=1,
279
+ )
280
+ if self.debug:
281
+ self.test_incoming_wav.seek(0, soundfile.SEEK_END)
282
+ self.test_incoming_wav.write(segment)
283
+
284
+ segment = segment.T
285
+ segment, new_sample_rate = self.convert_waveform(
286
+ segment,
287
+ sample_rate,
288
+ normalize_volume=False,
289
+ to_mono=True,
290
+ to_sample_rate=MODEL_SAMPLE_RATE,
291
+ )
292
+
293
+ assert MODEL_SAMPLE_RATE == new_sample_rate
294
+ segment = segment.squeeze(axis=0)
295
+ return segment, new_sample_rate
296
+
297
+ def process_pipeline_impl(self, input_segment):
298
+ try:
299
+ with torch.no_grad():
300
+ output_segment = OutputSegments(
301
+ self.agent.pushpop(input_segment, self.states)
302
+ )
303
+ if (
304
+ self.get_states_root().first_input_ts is not None
305
+ and self.first_input_ts is None
306
+ ):
307
+ # TODO: this is hacky
308
+ self.first_input_ts = self.get_states_root().first_input_ts
309
+
310
+ if not output_segment.is_empty:
311
+ self.output_queue.put_nowait(output_segment)
312
+
313
+ if output_segment.finished:
314
+ self.debug_log("OUTPUT SEGMENT IS FINISHED. Resetting states.")
315
+
316
+ self.reset_states()
317
+
318
+ if self.debug:
319
+ # when we rebuild states, this value is reset to whatever
320
+ # is in the system dir config, which defaults debug=False.
321
+ self.get_states_root().debug = True
322
+ except Exception as e:
323
+ logger.error(f"Got exception while processing pipeline: {e}")
324
+ traceback.print_exc()
325
+ return input_segment
326
+
327
+ def process_pipeline_loop(self):
328
+ if self.close:
329
+ return # closes the thread
330
+
331
+ self.debug_log("processing_pipeline")
332
+ while not self.close:
333
+ input_segment = self.get_input_segment()
334
+ if input_segment is None:
335
+ if self.get_states_root().is_fresh_state: # TODO: this is hacky
336
+ time.sleep(0.3)
337
+ else:
338
+ time.sleep(0.03)
339
+ continue
340
+ self.process_pipeline_impl(input_segment)
341
+ self.debug_log("finished processing_pipeline")
342
+
343
+ def process_pipeline_once(self):
344
+ if self.close:
345
+ return
346
+
347
+ self.debug_log("processing pipeline once")
348
+ input_segment = self.get_input_segment()
349
+ if input_segment is None:
350
+ return
351
+ self.process_pipeline_impl(input_segment)
352
+ self.debug_log("finished processing_pipeline_once")
353
+
354
+ def get_output_segment(self):
355
+ if self.output_queue.empty():
356
+ return None
357
+
358
+ output_chunk = self.output_queue.get_nowait()
359
+ self.output_queue.task_done()
360
+ return output_chunk
361
+
362
+ def start(self):
363
+ self.debug_log("starting transcoder in a thread")
364
+ threading.Thread(target=self.process_pipeline_loop).start()
365
+
366
+ def first_translation_time(self):
367
+ return round((self.first_output_ts - self.first_input_ts) / 1000, 2)
368
+
369
+ def get_buffered_output(self) -> SpeechAndTextOutput:
370
+ now = time.time() * 1000
371
+ self.debug_log(f"get_buffered_output queue size: {self.output_queue.qsize()}")
372
+ while not self.output_queue.empty():
373
+ tmp_out = self.get_output_segment()
374
+ if tmp_out and tmp_out.compute_length(self.g2p) > 0:
375
+ if len(self.output_buffer) == 0:
376
+ self.last_output_ts = now
377
+ self._populate_output_buffer(tmp_out)
378
+ self._increment_output_buffer_size(tmp_out)
379
+
380
+ if tmp_out.finished:
381
+ self.debug_log("tmp_out.finished")
382
+ res = self._gather_output_buffer_data(final=True)
383
+ self.debug_log(f"gathered output data: {res}")
384
+ self.output_buffer = []
385
+ self.increment_output_buffer_size = 0
386
+ self.last_output_ts = now
387
+ self.first_output_ts = now
388
+ return res
389
+ else:
390
+ self.debug_log("tmp_out.compute_length is not > 0")
391
+
392
+ if len(self.output_buffer) > 0 and (
393
+ now - self.last_output_ts >= self.output_buffer_idle_ms
394
+ or self.output_buffer_cur_size >= self.output_buffer_size_limit
395
+ ):
396
+ self.debug_log(
397
+ "[get_buffered_output] output_buffer is not empty. getting res to return."
398
+ )
399
+ self.last_output_ts = now
400
+ res = self._gather_output_buffer_data(final=False)
401
+ self.debug_log(f"gathered output data: {res}")
402
+ self.output_buffer = []
403
+ self.output_buffer_phoneme_count = 0
404
+ self.first_output_ts = now
405
+ return res
406
+ else:
407
+ self.debug_log("[get_buffered_output] output_buffer is empty...")
408
+ return None
409
+
410
+ def _gather_output_buffer_data(self, final):
411
+ output = SpeechAndTextOutput()
412
+ output.final = final
413
+ output = OutputSegments.join_output_buffer(self.output_buffer, output)
414
+ return output
415
+
416
+ def _increment_output_buffer_size(self, segment: OutputSegments):
417
+ self.output_buffer_cur_size += segment.compute_length(self.g2p)
418
+
419
+ def _populate_output_buffer(self, segment: OutputSegments):
420
+ self.output_buffer.append(segment.segments)
421
+
422
+ def _compute_phoneme_count(self, string: str) -> int:
423
+ return len([x for x in self.g2p(string) if x != " "])
seamless_server/src/speech_and_text_output.py ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Provides a container to return both speech and text output from our model at the same time
2
+
3
+
4
+ class SpeechAndTextOutput:
5
+ def __init__(
6
+ self,
7
+ text: str = None,
8
+ speech_samples: list = None,
9
+ speech_sample_rate: float = None,
10
+ final: bool = False,
11
+ ):
12
+ self.text = text
13
+ self.speech_samples = speech_samples
14
+ self.speech_sample_rate = speech_sample_rate
15
+ self.final = final
seamless_server/src/timing.ipynb ADDED
@@ -0,0 +1,169 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "cells": [
3
+ {
4
+ "cell_type": "code",
5
+ "execution_count": 10,
6
+ "metadata": {},
7
+ "outputs": [
8
+ {
9
+ "data": {
10
+ "text/plain": [
11
+ "[Text(1, 0, 'nonincremental'), Text(2, 0, 'incremental')]"
12
+ ]
13
+ },
14
+ "execution_count": 10,
15
+ "metadata": {},
16
+ "output_type": "execute_result"
17
+ },
18
+ {
19
+ "data": {
20
+ "image/png": "iVBORw0KGgoAAAANSUhEUgAAAiMAAAGdCAYAAADAAnMpAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjcuMSwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/bCgiHAAAACXBIWXMAAA9hAAAPYQGoP6dpAAAktElEQVR4nO3de3BU5f3H8c9mkUAwiYLkggSTGjBQIuEmJDSStLQMRYediEWUQalYFbAKKG3QSsefJVZJgRlBRMpoRQQNMXa2XqAoGGALQsAxCsotBCUJXmrCNdHd/f3hZGVrbptkeXY379fMGd1znnP2G/RkPzz7PM+xuN1utwAAAAwJM10AAADo2AgjAADAKMIIAAAwijACAACMIowAAACjCCMAAMAowggAADCKMAIAAIzqZLqAlnC5XDpx4oQiIyNlsVhMlwMAAFrA7Xbr1KlT6tWrl8LCGu//CIowcuLECSUkJJguAwAAtMLx48fVu3fvRo8HRRiJjIyU9P0PExUVZbgaAADQEjU1NUpISPB8jjcmKMJI/VczUVFRhBEAAIJMc0MsGMAKAACMIowAAACjCCMAAMAowggAADCKMAIAAIwijAAAAKMIIwAAwCjCCAAAMCooFj1Dx+F0OlVcXKyKigrFx8crMzNTVqvVdFkAAD+iZwQBo7CwUMnJycrOztatt96q7OxsJScnq7Cw0HRpAAA/IowgIBQWFmrixIlKTU2Vw+HQqVOn5HA4lJqaqokTJxJIACCEWdxut9t0Ec2pqalRdHS0qqureTZNCHI6nUpOTlZqaqqKioq8HjPtcrlks9lUWlqqgwcP8pUNAASRln5+0zMC44qLi1VWVqb58+d7BRFJCgsLU25uro4ePari4mJDFQIA/IkwAuMqKiokSQMHDmzweP3++nYAgNBCGIFx8fHxkqTS0tIGj9fvr28HAAgthBEYl5mZqcTERC1cuFAul8vrmMvlUl5enpKSkpSZmWmoQgCAPxFGYJzValV+fr7sdrtsNpvXbBqbzSa73a5FixYxeBUAQhSLniEg5OTkqKCgQHPnzlVGRoZnf1JSkgoKCpSTk2OwOgCAPzG1FwGFFVgBIHS09PObnhEEFKvVqqysLNNlAAAuIsaMAAAAowgjAADAKMIIAAAwijACAACMIowAAACjCCMAAMAowggAADCKMAIAAIwijAAAAKMIIwAAwCjCCAAAMIowAgAAjCKMAAAAowgjAADAKMIIAAAwijACAACMIowAAACjCCMAAMAowggAADCKMAIAAIwijAAAAKMIIwAAwCjCCAAAMIowAgAAjPIpjOTl5Wn48OGKjIxUTEyMbDabPvnkk2bPe/XVV5WSkqIuXbooNTVVb7zxRqsLBgAAocWnMLJ161bNnDlT//nPf7Rp0yZ9++23+tWvfqUzZ840es6OHTs0efJk3Xnnndq7d69sNptsNptKS0vbXDwAAAh+Frfb7W7tyV988YViYmK0detWXX/99Q22mTRpks6cOSO73e7ZN3LkSKWlpWnFihUtep+amhpFR0erurpaUVFRrS0XAABcRC39/G7TmJHq6mpJUvfu3Rtt43A4NGbMGK99Y8eOlcPhaMtbAwCAENGptSe6XC498MADGjVqlAYOHNhou8rKSsXGxnrti42NVWVlZaPn1NbWqra21vO6pqamtWUCAIAA1+qekZkzZ6q0tFTr1q1rz3okfT9QNjo62rMlJCS0+3sAAIDA0KowMmvWLNntdr377rvq3bt3k23j4uJUVVXlta+qqkpxcXGNnpObm6vq6mrPdvz48daUCQAAgoBPYcTtdmvWrFl67bXX9M477ygpKanZc9LT07V582avfZs2bVJ6enqj54SHhysqKsprAwAAocmnMSMzZ87U2rVr9frrrysyMtIz7iM6Olpdu3aVJE2dOlVXXnml8vLyJEn333+/Ro8erfz8fI0fP17r1q3T7t27tXLlynb+UQAAQDDyqWfkmWeeUXV1tbKyshQfH+/Z1q9f72lTXl6uiooKz+uMjAytXbtWK1eu1KBBg1RQUKCioqImB70CAICOo03rjFwsrDMCAEDwuSjrjAAAALQVYQQAABhFGAEAAEYRRgAAgFGEEQAAYBRhBAAAGEUYAQAARhFGAACAUYQRAABgFGEEAAAYRRgBAABGEUYAAIBRhBEAAGAUYQQAABhFGAEAAEYRRgAAgFGEEQAAYBRhBAAAGEUYAQAARhFGAACAUYQRAABgFGEEAAAYRRgBAABGEUYAAIBRhBEAAGAUYQQAABhFGAEAAEYRRgAAgFGEEQAAYBRhBAAAGEUYAQAARhFGAACAUYQRAABgFGEEAAAYRRgBAABGEUYAAIBRhBEAAGAUYQQAABhFGAEAAEYRRgAAgFGEEQAAYBRhBAAAGEUYAQAARhFGAACAUYQRAABgFGEEAAAYRRgBAABGEUYAAIBRhBEAAGAUYQQAABhFGAEAAEYRRgAAgFGEEQAAYBRhBAAAGEUYAQAARhFGAACAUYQRAABgFGEEAAAYRRgBAABGEUYAAIBRhBEAAGAUYQQAABjVyXQBwIWcTqeKi4tVUVGh+Ph4ZWZmymq1mi4LAOBH9IwgYBQWFio5OVnZ2dm69dZblZ2dreTkZBUWFpouDQDgR4QRBITCwkJNnDhRqampcjgcOnXqlBwOh1JTUzVx4kQCCQCEMIvb7XabLqI5NTU1io6OVnV1taKiokyXg3bmdDqVnJys1NRUFRUVKSzsh4zscrlks9lUWlqqgwcP8pUNAASRln5+0zMC44qLi1VWVqb58+d7BRFJCgsLU25uro4ePari4mJDFQIA/MnnMPLee+/pxhtvVK9evWSxWFRUVNRk+y1btshisfxoq6ysbG3NCDEVFRWSpIEDBzZ4vH5/fTsAQGjxOYycOXNGgwYN0rJly3w675NPPlFFRYVni4mJ8fWtEaLi4+MlSaWlpQ0er99f3w4AEFp8nto7btw4jRs3zuc3iomJ0WWXXebzeQh9mZmZSkxM1MKFCxscM5KXl6ekpCRlZmYarBIA4C8XbcxIWlqa4uPj9ctf/lLbt29vsm1tba1qamq8NoQuq9Wq/Px82e122Ww2r9k0NptNdrtdixYtYvAqAIQov4eR+Ph4rVixQhs2bNCGDRuUkJCgrKwslZSUNHpOXl6eoqOjPVtCQoK/y4RhOTk5Kigo0IcffqiMjAxFRUUpIyNDpaWlKigoUE5OjukSAQB+0qapvRaLRa+99ppsNptP540ePVp9+vTRiy++2ODx2tpa1dbWel7X1NQoISGBqb0dACuwAkDoaOnUXiPLwV933XXatm1bo8fDw8MVHh5+EStCoLBarcrKyjJdBgDgIjKyzsi+ffuYGQEAACS1omfk9OnTOnTokOf10aNHtW/fPnXv3l19+vRRbm6uPv/8c/3jH/+QJC1ZskRJSUn66U9/qvPnz2vVqlV65513tHHjxvb7KQAAQNDyOYzs3r1b2dnZntdz5syRJN1+++16/vnnVVFRofLycs/xuro6zZ07V59//rkiIiJ07bXX6t///rfXNQAAQMfFs2kAAIBf8GwaAAAQFAgjAADAKMIIAAAwijACAACMIowAAACjCCMAAMAowggAADCKMAIAAIwijAAAAKMIIwAAwCjCCAAAMIowAgAAjCKMAAAAowgjAADAKMIIAAAwijACAACMIowAAACjCCMAAMAowggAADCKMAIAAIwijAAAAKMIIwAAwCjCCAAAMIowAgAAjCKMAAAAowgjAADAKMIIAAAwijACAACMIowAAACjCCMAAMAowggAADCKMAIAAIwijAAAAKMIIwAAwCjCCAAAMIowAgAAjCKMAAAAowgjAADAKMIIAAAwijACAACMIowAAACjCCMAAMAowggAADCKMAIAAIwijAAAAKMIIwAAwCjCCAAAMIowAgAAjCKMAAAAowgjAADAKMIIAAAwijACAACMIowAAACjCCMAAMAowggAADCKMAIAAIwijAAAAKMIIwAAwCjCCAAAMIowAgAAjCKMAAAAowgjAADAKMIIAAAwijACAACMIowAAACjCCMAAMAon8PIe++9pxtvvFG9evWSxWJRUVFRs+ds2bJFQ4YMUXh4uJKTk/X888+3olQAABCKfA4jZ86c0aBBg7Rs2bIWtT969KjGjx+v7Oxs7du3Tw888ICmT5+ut99+2+diAQBA6Onk6wnjxo3TuHHjWtx+xYoVSkpKUn5+viSpf//+2rZtmxYvXqyxY8f6+vYAACDE+H3MiMPh0JgxY7z2jR07Vg6Ho9FzamtrVVNT47UBAIDQ5PcwUllZqdjYWK99sbGxqqmp0blz5xo8Jy8vT9HR0Z4tISHB32UCAABDAnI2TW5urqqrqz3b8ePHTZcEAAD8xOcxI76Ki4tTVVWV176qqipFRUWpa9euDZ4THh6u8PBwf5cGAAACgN97RtLT07V582avfZs2bVJ6erq/3xoAAAQBn8PI6dOntW/fPu3bt0/S91N39+3bp/Lycknff8UydepUT/t77rlHR44c0bx583TgwAEtX75cr7zyimbPnt0+PwEAAAhqPoeR3bt3a/DgwRo8eLAkac6cORo8eLAeffRRSVJFRYUnmEhSUlKS/vWvf2nTpk0aNGiQ8vPztWrVKqb1AgAASZLF7Xa7TRfRnJqaGkVHR6u6ulpRUVGmywEAAC3Q0s9vvw9gBQBAkpxOp4qLi1VRUaH4+HhlZmbKarWaLgsBICCn9gIAQkthYaGSk5OVnZ2tW2+9VdnZ2UpOTlZhYaHp0hAACCMAAL8qLCzUxIkTlZqaKofDoVOnTsnhcCg1NVUTJ04kkIAxIwAA/3E6nUpOTlZqaqqKiooUFvbD34FdLpdsNptKS0t18OBBvrIJQS39/KZnBADgN8XFxSorK9P8+fO9gogkhYWFKTc3V0ePHlVxcbGhChEICCMAAL+pqKiQJA0cOLDB4/X769uhYyKMAAD8Jj4+XpJUWlra4PH6/fXt0DERRgAAfpOZmanExEQtXLhQLpfL65jL5VJeXp6SkpKUmZlpqEIEAsIIAMBvrFar8vPzZbfbZbPZvGbT2Gw22e12LVq0iMGrHRyLngEA/ConJ0cFBQWaO3euMjIyPPuTkpJUUFCgnJwcg9UhEDC1FwBwUbACa8fDcvAISvyyAkKX1WpVVlaW6TIQgBgzgoDBctEA0DERRhAQWC4aADouxozAOJaLBoDQxHLwCBosFw0AHRthBMaxXDQAdGyEERjHctEA0LERRmAcy0UDQMdGGIFxLBcNAB0bi54hILBcNAB0XEztRUBhBVYACB1M7QUAAEGBMIKAwXLwANAxEUYQEFgOHgA6LsaMwLgLl4PfsGGDtm/f7hkzMmrUKN10000sBw8AQYgxIwga9cvBZ2RkqF+/fl5f0/Tr10/p6eksBw8AIYwwAuPql3mfP39+g1/TPPzww17tAAChhXVGYFxMTIwkadSoUV5P7R05cqSKioo0evRobdu2zdMOABBa6BlBwAuCYU0AgDYgjMC4kydPSpK2bdvW4HLw27dv92oHIDg5nU5t2bJFL7/8srZs2SKn02m6JAQIwgiMq38ab15enj788ENlZGQoKipKGRkZKi0t1cKFC73aAQg+rCOEphBGYFz9U3t37NihTz/9VO+++67Wrl2rd999V5988okcDgdP7QWCGOsIoTmsM4KAUP/L6oYbblBubq4GDhyo0tJS5eXlyW6387A8IEhduI7QhQPUJcnlcslms7GOUAhjnREElfqn9jb0NQ1BBAhe9esIzZ8/3yuISFJYWJhyc3NZRwhM7UXgyMnJ0YQJE3hqLxBC6tcHGjhwYIPH6/ezjlDHRs8IAMBv6geel5aWNni8fj8D1Ds2wggCBqPtgdBTP0B94cKFcrlcXsdcLpfy8vIYoA7CCAIDo+2B0GS1WpWfny+73d7gOkJ2u12LFi3i69gOjtk0MI7R9kDoKyws1Ny5c1VWVubZl5SUpEWLFjFAPYS19PObMALjtmzZouzsbDkcDo0cOfJHxx0OhzIyMvTuu+8qKyvr4hcIoF04nU4GqHcwLf38ZjYNjLtwtH1Dv6wYbQ+EBqvVyl8o0CDCCIyrH0X/9NNP69lnn/Xqxk1MTNTvfvc7r3YAgNDCAFYYl5mZqZ49e3pWXr1wgNvAgQM1f/58xcTEMNoeAEIUYQQBwWKxeP7d7XZ7NgBA6COMwLji4mKdPHlSeXl5Ki0t9VoO/qOPPtLChQt18uRJlosGgBBFGIFx9QNTZ82apUOHDnk9tffgwYOaNWuWVzsAQGghjMA4losGgI6NdUZgXP2iZ1dccYW++OILHTt2zHPsqquuUs+ePfXVV1+x6BkABJmWfn7TMwLjrFarbr75Zu3evVvnz5/XypUrdeLECa1cuVLnz5/X7t27NXHiRIIIAIQoekZg3IU9I19++eWPlovu0aMHPSMAEIRYgRVBo7i4WGVlZXr55Zc1fPjwH63AumvXLmVkZKi4uJjVGwEgBBFGYNyFy8E3tFw0y8EDQGhjzAiMYzYNAHRshBEYl5mZqcTERC1cuFAul8vrmMvlUl5enpKSklgOHgBCFGEExlmtVuXn58tut8tms3k9m8Zms8lut2vRokUMXgWAEMWYEQSEnJwcFRQUaO7cucrIyPDsT0pKUkFBgXJycgxWBwDwJ6b2IqA4nc4fzaahRwQAghNTexGUGppNAwAIbYwZAQAARhFGAACAUYQRAABgFGNGcNGcPXtWBw4caLbduXPnVFZWpsTERHXt2rXJtikpKYqIiGivEgEABhBGcNEcOHBAQ4cObddr7tmzR0OGDGnXawIALi7CCC6alJQU7dmzp9l2+/fv15QpU7RmzRr179+/2WsCAIIbYQQXTUREhE+9GP3796fXAwA6gFYNYF22bJkSExPVpUsXjRgxQrt27Wq07fPPPy+LxeK1denSpdUFAwCA0OJzGFm/fr3mzJmjBQsWqKSkRIMGDdLYsWN18uTJRs+JiopSRUWFZzt27FibigYAAKHD5zDyt7/9TXfddZemTZumAQMGaMWKFYqIiNDq1asbPcdisSguLs6zxcbGtqloAEDwqaur05IlS3TfffdpyZIlqqurM10SAoRPYaSurk579uzRmDFjfrhAWJjGjBkjh8PR6HmnT5/WVVddpYSEBE2YMEEfffRRk+9TW1urmpoarw0AELzmzZunbt26afbs2Xr66ac1e/ZsdevWTfPmzTNdGgKAT2Hkyy+/lNPp/FHPRmxsrCorKxs855prrtHq1av1+uuva82aNXK5XMrIyNBnn33W6Pvk5eUpOjrasyUkJPhSJgAggMybN09PPfWUevTooeeee04VFRV67rnn1KNHDz311FMEEvj21N4TJ07oyiuv1I4dO5Senu7ZP2/ePG3dulU7d+5s9hrffvut+vfvr8mTJ+v//u//GmxTW1ur2tpaz+uamholJCTw1N4OoqSkREOHDmUNESAE1NXVqVu3burRo4c+++wzder0wyTO7777Tr1799ZXX32lM2fOqHPnzgYrhT+09Km9PvWMXHHFFbJaraqqqvLaX1VVpbi4uBZd45JLLtHgwYN16NChRtuEh4crKirKawMABJ/ly5fru+++0+OPP+4VRCSpU6dOeuyxx/Tdd99p+fLlhipEIPApjHTu3FlDhw7V5s2bPftcLpc2b97s1VPSFKfTqQ8//FDx8fG+VQoACDqHDx+WJN1www0NHq/fX98OHZPPs2nmzJmj5557Ti+88IL279+ve++9V2fOnNG0adMkSVOnTlVubq6n/WOPPaaNGzfqyJEjKikp0ZQpU3Ts2DFNnz69/X4KAEBAuvrqqyVJdru9wdk0drvdqx06Jp9XYJ00aZK++OILPfroo6qsrFRaWpreeustz6DW8vJyhYX9kHH++9//6q677lJlZaUuv/xyDR06VDt27NCAAQPa76cAAASkGTNm6KGHHtL999+ve+65R06n03PswQcfVHh4uDp16qQZM2YYrBKm+TSA1ZSWDoBBaGAAKxBarrvuOr3//vuyWCy67bbbNHfuXOXn5+ull16S2+3W8OHDm1zJG8GrpZ/fPJsGAOA3dXV12rt3ryIiInT+/HmtWbNGa9askSRZrVaFh4dr7969qqurYzZNB9aqZ9MAANAS9bNpli5dqnPnzmnx4sWaNWuWFi9erLNnz2rx4sXMpgE9IwAA/7lwNk3nzp31wAMPeB1nNg0kekYAAH504WyahjCbBhIDWBGAGMAKhA5WYO3Y/LICKwAAvujcubNmz56tqqoq9e7dWytXrtSJEye0cuVK9e7dW1VVVZo9ezZBpINjzAgAwK+efPJJSdLixYt19913e/Z36tRJDz30kOc4Oi7CCADA75588kk9/vjjWr58uQ4fPqyrr75aM2bMoEcEkggjAICLpKHZNIDEmBEAAGAYPSMAgDY7e/asDhw40Gy7c+fOqaysTImJieratWuTbVNSUhQREdFeJSKAEUYAAG124MABDR06tF2vyfT+joMwAgBos5SUFO3Zs6fZdvv379eUKVO0Zs0a9e/fv9lromMgjAAA2iwiIsKnXoz+/fvT6wEPBrACAACjCCMAAMAowggAADCKMAIAAIwijAAAAKMIIwAAwCim9qLdHDx4UKdOnWrzdfbv3+/1z7aIjIxU375923wdAID/EEbQLg4ePKh+/fq16zWnTJnSLtf59NNPCSQAEMAII2gX9T0iLVlVsTm+PLuiKfUrPbZHbw0AwH8II2hX7bWq4qhRo9qhGgBAMGAAKwAAMIowAgAAjCKMAAAAowgjAADAKMIIAAAwijACAACMYmovAKBZrLAMfyKMAACaxArL8DfCCACgSaywDH8jjAAAWoQVluEvhBG0C8t35zU4Lkxdv/lUOhEY46K7fvOpBseFyfLdedOlAACaQBhBu+hyulwld18qvXe39J7par7XX1LJ3Zdq/+lySRmmywEANIIwgnZx/tI+GvLsab300kvqn5JiuhxJ0v4DB3Tbbbfp77/uY7oUAEATCCNoF+5OXbS30qVzl/WTeqWZLkeSdK7Spb2VLrk7dTFdCgCgCYHx5T4AAOiwCCMAAMAovqYBADSJ2XLwN8IIAKBJzJaDvxFGAABNYrYc/I0wAgBoErPl4G+B8eUfAADosOgZQbs4e/asJKmkpKTN12rPB2kBAAIfYQTt4sCBA5Kku+66y3AlPxYZGWm6BABAEwgjaBc2m02SlJKSooiIiDZdq/7R4O3xuPLIyEj17du3TdcAAPgXYQTt4oorrtD06dPb9Zrt9bhyAG3D17DwN8IIAKBJfA0LfyOMAACaxNew8DfCCACgSXwNC39jnREAAGAUYQQAABhFGAEAAEYRRgAAgFGEEQAAYBRhBAAAGEUYAQAARhFGAACAUYQRAABgFCuw4qI5e/as5xkXTal/AFZLHoTVHstTA2g77m+0hcXtdrtNF9GcmpoaRUdHq7q6WlFRUabLQSuVlJRo6NCh7XrNPXv2sKQ0EAC4v9GQln5+0zOCiyYlJUV79uxptp0vjxhPSUlpr/IAtAH3N9qCnhEAAOAXLf38ZgArAAAwqlVhZNmyZUpMTFSXLl00YsQI7dq1q8n2r776qlJSUtSlSxelpqbqjTfeaFWxAAAg9PgcRtavX685c+ZowYIFKikp0aBBgzR27FidPHmywfY7duzQ5MmTdeedd2rv3r2y2Wyy2WwqLS1tc/EAACD4+TxmZMSIERo+fLiefvppSZLL5VJCQoLuu+8+/fGPf/xR+0mTJunMmTOy2+2efSNHjlRaWppWrFjRovdkzAgAAMHHL2NG6urqtGfPHo0ZM+aHC4SFacyYMXI4HA2e43A4vNpL0tixYxttL0m1tbWqqanx2gAAQGjyKYx8+eWXcjqdio2N9dofGxurysrKBs+prKz0qb0k5eXlKTo62rMlJCT4UiYAAAgiATmbJjc3V9XV1Z7t+PHjpksCAAB+4tOiZ1dccYWsVquqqqq89ldVVSkuLq7Bc+Li4nxqL0nh4eEKDw/3pTQAABCkfOoZ6dy5s4YOHarNmzd79rlcLm3evFnp6ekNnpOenu7VXpI2bdrUaHsAANCx+Lwc/Jw5c3T77bdr2LBhuu6667RkyRKdOXNG06ZNkyRNnTpVV155pfLy8iRJ999/v0aPHq38/HyNHz9e69at0+7du7Vy5cr2/UkAAEBQ8jmMTJo0SV988YUeffRRVVZWKi0tTW+99ZZnkGp5ebnCwn7ocMnIyNDatWv1yCOPaP78+erbt6+Kioo0cODA9vspAABA0OLZNAAAwC94Ng0AAAgKPn9NY0J95w2LnwEAEDzqP7eb+xImKMLIqVOnJInFzwAACEKnTp1SdHR0o8eDYsyIy+XSiRMnFBkZKYvFYroc+FlNTY0SEhJ0/PhxxggBIYb7u2Nxu906deqUevXq5TW55X8FRc9IWFiYevfubboMXGRRUVH8sgJCFPd3x9FUj0g9BrACAACjCCMAAMAowggCTnh4uBYsWMDziYAQxP2NhgTFAFYAABC66BkBAABGEUYAAIBRhBEAAGAUYQTNysrK0gMPPGC6jKB0xx13yGazmS4DHQj3a+twr5oVFIuewazCwkJdcsklpsswKisrS2lpaVqyZInpUoAmdfT7lXs1OBFG0Kzu3bv79fp1dXXq3LmzX98D6Cj8eb9yr8Jf+JomyGVlZen3v/+95s2bp+7duysuLk5//vOfPcfLy8s1YcIEXXrppYqKitJvfvMbVVVVeY7/+c9/Vlpaml588UUlJiYqOjpat9xyi+fhhPXvcWG3b2JiohYuXKjf/va3ioyMVJ8+fbRy5Uqvuj777DNNnjxZ3bt3V7du3TRs2DDt3LnT6z1XrVqlpKQkdenSRZL0zTffaPr06erZs6eioqL085//XB988MGPal29erX69OmjSy+9VDNmzJDT6dSTTz6puLg4xcTE6C9/+YtXLS29bmN/BnfccYe2bt2qpUuXymKxyGKxqKysTE6nU3feeaeSkpLUtWtXXXPNNVq6dGkr/0sC7ePC+5V7lXs1WBBGQsALL7ygbt26aefOnXryySf12GOPadOmTXK5XJowYYK+/vprbd26VZs2bdKRI0c0adIkr/MPHz6soqIi2e122e12bd26VU888UST75mfn69hw4Zp7969mjFjhu6991598sknkqTTp09r9OjR+vzzz/XPf/5TH3zwgebNmyeXy+U5/9ChQ9qwYYMKCwu1b98+SdLNN9+skydP6s0339SePXs0ZMgQ/eIXv9DXX3/tVeubb76pt956Sy+//LL+/ve/a/z48frss8+0detW/fWvf9Ujjzzi+WXqy3Ub+zNYunSp0tPTddddd6miokIVFRVKSEiQy+VS79699eqrr+rjjz/Wo48+qvnz5+uVV15p3X9IwA+4V7lXg4IbQW306NHun/3sZ177hg8f7v7DH/7g3rhxo9tqtbrLy8s9xz766CO3JPeuXbvcbrfbvWDBAndERIS7pqbG0+ahhx5yjxgxwus97r//fs/rq666yj1lyhTPa5fL5Y6JiXE/88wzbrfb7X722WfdkZGR7q+++qrBmhcsWOC+5JJL3CdPnvTsKy4udkdFRbnPnz/v1fbqq692P/vss43WOnbsWHdiYqLb6XR69l1zzTXuvLy8Nl23uT+DxsycOdN90003eV7ffvvt7gkTJjR7HtBeLvx/lXu1cdyrgYUxIyHg2muv9XodHx+vkydPav/+/UpISFBCQoLn2IABA3TZZZdp//79Gj58uKTvu3IjIyN/dH5L39NisSguLs5zzr59+zR48OAmv7u+6qqr1LNnT8/rDz74QKdPn1aPHj282p07d06HDx/2vP7fWmNjY2W1Wr0eTR0bG+uppbXXbcmfgSQtW7ZMq1evVnl5uc6dO6e6ujqlpaU1ex5wsXCvfo97NbARRkLA/46ct1gsXt2s/ji/qXO6du3a7Ht269bN6/Xp06cVHx+vLVu2/KjtZZdd1uT7NlVLW67b3J/BunXr9OCDDyo/P1/p6emKjIzUU0895dXtDJjGvcq9GgwIIyGsf//+On78uI4fP+7pHfn444/1zTffaMCAAX5732uvvVarVq3S119/3eKR/UOGDFFlZaU6deqkxMTEdqulva7buXNnOZ1Or33bt29XRkaGZsyY4dl34d/ggEDHvYpAwQDWEDZmzBilpqbqtttuU0lJiXbt2qWpU6dq9OjRGjZsmN/ed/LkyYqLi5PNZtP27dt15MgRbdiwQQ6Ho8la09PTZbPZtHHjRpWVlWnHjh16+OGHtXv37lbX0l7XTUxM1M6dO1VWVqYvv/xSLpdLffv21e7du/X222/r008/1Z/+9Ce9//77ra4VuNi4VxEoCCMhzGKx6PXXX9fll1+u66+/XmPGjNFPfvITrV+/3q/v27lzZ23cuFExMTH69a9/rdTUVD3xxBOyWq1N1vrGG2/o+uuv17Rp09SvXz/dcsstOnbsmGJjY1tdS3td98EHH5TVatWAAQPUs2dPlZeX6+6771ZOTo4mTZqkESNG6KuvvvL6mxcQ6LhXESgsbrfbbboIAADQcdEzAgAAjCKMAAAAowgjAADAKMIIAAAwijACAACMIowAAACjCCMAAMAowggAADCKMAIAAIwijAAAAKMIIwAAwCjCCAAAMOr/ASXbw3ES3Qk1AAAAAElFTkSuQmCC",
21
+ "text/plain": [
22
+ "<Figure size 640x480 with 1 Axes>"
23
+ ]
24
+ },
25
+ "metadata": {},
26
+ "output_type": "display_data"
27
+ }
28
+ ],
29
+ "source": [
30
+ "import matplotlib.pyplot as plt\n",
31
+ "import numpy as np\n",
32
+ "\n",
33
+ "nonincremental = [0.144, 0.268, 0.175, 0.292, 0.161, 0.147, 0.238, 0.159, 0.26, 0.161, 0.511, 0.263, 0.149, 0.141, 0.427, 0.06, 0.055, 0.056, 0.145, 0.142, 0.137, 0.302, 0.146, 0.35, 0.256, 0.162, 0.161, 0.33, 0.176, 0.4, 0.294, 0.176, 0.308, 0.206, 0.349, 0.3, 0.326, 0.216, 0.236, 0.852, 0.057, 0.145, 0.386, 0.157, 0.154, 0.351, 0.321, 0.161, 0.158, 0.271, 0.163, 0.619, 0.052, 0.055, 0.164, 0.147, 0.145, 0.214, 0.179, 0.147, 0.398, 0.351, 0.162, 0.158, 0.293, 0.292, 0.169, 0.202, 0.427, 0.205, 0.335, 0.197, 0.211, 0.217, 0.23, 0.225, 0.495, 0.26, 0.346, 0.243, 0.271, 0.259, 0.266, 0.256, 0.288, 0.492, 0.303, 0.294, 0.322, 0.317, 0.299, 1.524, 0.059, 0.056, 0.147, 0.219, 0.161, 0.334, 0.159, 0.175, 0.375, 0.16, 0.177, 0.337, 0.273, 0.184, 0.183, 0.778, 0.228, 0.305, 0.36, 0.159, 0.157, 0.469, 0.057, 0.055, 0.059, 0.059, 0.056, 0.059, 0.063, 0.056, 0.059, 0.056, 0.154, 0.145, 0.208, 0.15, 0.428, 0.281, 0.146, 0.151, 0.324, 0.154, 0.208, 0.141, 0.163, 0.359, 0.282, 0.271, 0.142, 0.529, 0.164, 0.224, 0.139, 0.139, 0.159, 0.344, 0.242, 0.26, 0.155, 0.498, 0.053, 0.152, 0.214, 0.15, 0.148, 0.147, 0.356, 0.252, 0.149, 0.43, 0.057, 0.059, 0.154, 0.212, 0.17, 0.14, 0.144, 0.362, 0.164, 0.216, 0.21, 0.166, 0.155, 0.281, 0.155, 0.314, 0.157, 0.544, 0.253, 0.343, 0.272, 0.186, 0.19, 0.327, 0.212, 0.216, 0.374, 0.241, 0.328, 0.463, 0.424, 0.318, 0.244, 0.245, 0.251, 0.256, 0.546, 0.287, 0.269, 0.316, 0.416, 0.303, 0.407, 0.305, 0.31, 0.311, 0.319, 0.326, 0.343, 0.344, 0.447, 0.685, 0.385, 0.359, 0.369, 0.654, 0.404, 2.228, 0.115, 0.166, 0.216, 0.141, 0.138, 0.268, 0.156, 0.345, 0.314, 0.183, 0.185, 0.193, 0.297, 0.396, 0.203, 0.179, 0.289, 0.187, 0.322, 0.203, 0.204, 0.46, 0.256, 0.244, 0.759]\n",
34
+ "# incremental = [0.139, 0.262, 0.16, 0.347, 0.177, 0.155, 0.16, 0.24, 0.201, 0.165, 0.489, 0.198, 0.227, 0.153, 0.156, 0.407, 0.056, 0.058, 0.143, 0.143, 0.219, 0.155, 0.328, 0.224, 0.335, 0.163, 0.247, 0.277, 0.197, 0.348, 0.337, 0.193, 0.195, 0.209, 0.293, 0.336, 0.226, 0.224, 0.206, 0.841, 0.066, 0.165, 0.261, 0.213, 0.141, 0.33, 0.186, 0.152, 0.159, 0.334, 0.175, 0.16, 0.583, 0.055, 0.155, 0.206, 0.141, 0.34, 0.152, 0.144, 0.214, 0.274, 0.307, 0.162, 0.235, 0.277, 0.172, 0.206, 0.189, 0.337, 0.46, 0.223, 0.209, 0.209, 0.214, 0.216, 0.371, 0.224, 0.375, 0.338, 0.266, 0.244, 0.269, 0.274, 0.273, 0.271, 0.283, 0.283, 0.305, 0.434, 0.33, 1.08, 0.065, 0.059, 0.134, 0.15, 0.145, 0.206, 0.354, 0.165, 0.255, 0.159, 0.286, 0.275, 0.16, 0.175, 0.181, 0.533, 0.201, 0.18, 0.185, 0.373, 0.16, 0.162, 0.455, 0.06, 0.057, 0.053, 0.057, 0.061, 0.063, 0.047, 0.059, 0.056, 0.057, 0.158, 0.154, 0.141, 0.383, 0.151, 0.142, 0.141, 0.359, 0.143, 0.225, 0.155, 0.151, 0.268, 0.334, 0.152, 0.206, 0.161, 0.401, 0.235, 0.275, 0.187, 0.151, 0.152, 0.287, 0.244, 0.272, 0.159, 0.559, 0.185, 0.156, 0.152, 0.146, 0.143, 0.263, 0.161, 0.316, 0.163, 0.438, 0.058, 0.167, 0.241, 0.161, 0.162, 0.151, 0.371, 0.062, 0.156, 0.215, 0.149, 0.145, 0.293, 0.165, 0.328, 0.36, 0.238, 0.387, 0.189, 0.437, 0.207, 0.228, 0.233, 0.365, 0.358, 0.381, 0.226, 0.225, 0.219, 0.448, 0.402, 0.246, 0.322, 0.289, 0.272, 0.394, 0.291, 0.365, 0.298, 0.409, 0.319, 0.323, 0.431, 0.339, 0.326, 0.311, 0.319, 0.325, 0.333, 0.367, 0.364, 0.594, 0.382, 0.595, 0.411, 0.414, 0.419, 1.668, 0.153, 0.23, 0.263, 0.147, 0.281, 0.156, 0.172, 0.354, 0.166, 0.199, 0.244, 0.174, 0.182, 0.294, 0.25, 0.184, 0.182, 0.357, 0.329, 0.226, 0.417, 0.239, 0.214, 0.579]\n",
35
+ "# incremental = [0.27, 0.147, 0.285, 0.146, 0.232, 0.168, 0.226, 0.267, 0.183, 0.151, 0.481, 0.174, 0.218, 0.142, 0.385, 0.061, 0.056, 0.155, 0.162, 0.219, 0.152, 0.279, 0.217, 0.425, 0.235, 0.162, 0.161, 0.3, 0.176, 0.174, 0.349, 0.305, 0.212, 0.221, 0.364, 0.223, 0.408, 0.467, 0.243, 0.707, 0.18, 0.143, 0.212, 0.181, 0.322, 0.265, 0.151, 0.157, 0.22, 0.291, 0.166, 0.55, 0.052, 0.154, 0.208, 0.147, 0.369, 0.163, 0.238, 0.151, 0.325, 0.289, 0.17, 0.234, 0.178, 0.269, 0.196, 0.216, 0.392, 0.358, 0.21, 0.22, 0.216, 0.302, 0.223, 0.256, 0.533, 0.273, 0.322, 0.25, 0.248, 0.314, 0.3, 0.284, 0.439, 0.288, 0.299, 0.298, 0.299, 0.304, 1.138, 0.18, 0.056, 0.054, 0.151, 0.135, 0.219, 0.281, 0.254, 0.168, 0.155, 0.301, 0.301, 0.276, 0.173, 0.161, 0.523, 0.242, 0.217, 0.142, 0.261, 0.147, 0.155, 0.47, 0.067, 0.063, 0.059, 0.223, 0.06, 0.057, 0.058, 0.062, 0.06, 0.058, 0.157, 0.148, 0.147, 0.484, 0.188, 0.145, 0.147, 0.344, 0.054, 0.14, 0.217, 0.143, 0.298, 0.151, 0.226, 0.155, 0.157, 0.43, 0.217, 0.214, 0.152, 0.153, 0.233, 0.285, 0.224, 0.329, 0.187, 0.436, 0.287, 0.143, 0.263, 0.171, 0.172, 0.36, 0.269, 0.676, 0.22, 0.411, 0.056, 0.161, 0.136, 0.255, 0.152, 0.156, 0.147, 0.402, 0.181, 0.208, 0.156, 0.149, 0.3, 0.158, 0.263, 0.366, 0.161, 0.41, 0.42, 0.459, 0.18, 0.177, 0.188, 0.305, 0.228, 0.354, 0.234, 0.248, 0.222, 0.437, 0.399, 0.294, 0.375, 0.266, 0.257, 0.478, 0.315, 0.388, 0.301, 0.362, 0.315, 0.315, 0.439, 0.311, 0.325, 0.344, 0.375, 0.367, 0.349, 0.333, 0.366, 0.556, 0.393, 0.599, 0.393, 0.766, 0.403, 1.396, 0.423, 0.232, 0.258, 0.145, 0.283, 0.156, 0.155, 0.27, 0.278, 0.172, 0.16, 0.167, 0.238, 0.322, 0.188, 0.181, 0.198, 0.275, 0.31, 0.208, 0.468, 0.223, 0.261, 0.62, ]\n",
36
+ "\n",
37
+ "# incremental orig [vad_update + states]\n",
38
+ "# incremental = [0.123, 0.194, 0.206, 0.163, 0.145, 0.15, 0.142, 0.226, 0.136, 0.139, 0.131, 0.274, 0.139, 0.172, 0.125, 0.325, 0.053, 0.056, 0.054, 0.123, 0.124, 0.126, 0.255, 0.129, 0.258, 0.19, 0.139, 0.141, 0.138, 0.155, 0.263, 0.159, 0.166, 0.163, 0.174, 0.196, 0.198, 0.193, 0.208, 0.192, 0.56, 0.068, 0.33, 0.163, 0.124, 0.195, 0.136, 0.125, 0.124, 0.13, 0.146, 0.143, 0.423, 0.063, 0.125, 0.139, 0.12, 0.157, 0.127, 0.2, 0.138, 0.316, 0.132, 0.202, 0.203, 0.218, 0.142, 0.153, 0.149, 0.137, 0.147, 0.159, 0.152, 0.201, 0.168, 0.209, 0.171, 0.201, 0.223, 0.248, 0.131, 0.199, 0.137, 0.135, 0.176, 0.201, 0.135, 0.193, 0.131, 0.177, 0.145, 0.145, 0.3, 0.056, 0.058, 0.176, 0.162, 0.121, 0.245, 0.144, 0.125, 0.203, 0.143, 0.144, 0.153, 0.195, 0.146, 0.35, 0.124, 0.154, 0.124, 0.265, 0.131, 0.284, 0.236, 0.068, 0.061, 0.054, 0.056, 0.067, 0.056, 0.05, 0.049, 0.061, 0.054, 0.136, 0.172, 0.119, 0.312, 0.123, 0.122, 0.123, 0.26, 0.059, 0.123, 0.149, 0.118, 0.119, 0.184, 0.192, 0.133, 0.133, 0.207, 0.118, 0.165, 0.12, 0.113, 0.12, 0.226, 0.136, 0.17, 0.131, 0.282, 0.054, 0.128, 0.174, 0.133, 0.156, 0.229, 0.218, 0.15, 0.128, 0.226, 0.054, 0.117, 0.126, 0.182, 0.128, 0.127, 0.146, 0.209, 0.138, 0.16, 0.129, 0.123, 0.21, 0.132, 0.144, 0.128, 0.15, 0.377, 0.114, 0.123, 0.205, 0.123, 0.125, 0.266, 0.175, 0.298, 0.131, 0.183, 0.143, 0.157, 0.168, 0.174, 0.17, 0.168, 0.187, 0.186, 0.182, 0.186, 0.211, 0.205, 0.218, 0.226, 0.223, 0.211, 0.235, 0.227, 0.23, 0.255, 0.265, 0.274, 0.271, 0.298, 0.265, 0.614, 0.297, 0.32, 0.302, 1.153, 0.09, 0.16, 0.123, 0.17, 0.242, 0.13, 0.135, 0.21, 0.125, 0.172, 0.179, 0.139, 0.15, 0.153, 0.157, 0.145, 0.15, 0.166, 0.17, 0.167, 0.365, 0.181, 0.185, 0.217, 0.442]\n",
39
+ "\n",
40
+ "# incremental 1\n",
41
+ "# incremental = [0.166, 0.141, 0.319, 0.247, 0.272, 0.15, 0.149, 0.188, 0.143, 0.146, 0.283, 0.141, 0.166, 0.122, 0.246, 0.054, 0.055, 0.13, 0.122, 0.172, 0.131, 0.224, 0.173, 0.19, 0.143, 0.217, 0.15, 0.155, 0.152, 0.169, 0.161, 0.176, 0.175, 0.305, 0.253, 0.131, 0.18, 0.136, 0.137, 0.318, 0.064, 0.123, 0.165, 0.124, 0.19, 0.199, 0.132, 0.141, 0.133, 0.2, 0.14, 0.327, 0.054, 0.136, 0.17, 0.125, 0.228, 0.146, 0.13, 0.135, 0.178, 0.21, 0.227, 0.128, 0.125, 0.167, 0.136, 0.177, 0.265, 0.196, 0.158, 0.144, 0.149, 0.144, 0.207, 0.162, 0.17, 0.167, 0.237, 0.196, 0.18, 0.185, 0.192, 0.198, 0.187, 0.205, 0.205, 0.226, 0.226, 0.213, 0.609, 0.062, 0.06, 0.053, 0.126, 0.122, 0.176, 0.191, 0.13, 0.147, 0.178, 0.136, 0.206, 0.144, 0.143, 0.142, 0.386, 0.128, 0.166, 0.136, 0.228, 0.131, 0.132, 0.22, 0.056, 0.063, 0.07, 0.07, 0.064, 0.079, 0.065, 0.059, 0.053, 0.062, 0.128, 0.129, 0.125, 0.241, 0.153, 0.135, 0.124, 0.227, 0.055, 0.132, 0.173, 0.126, 0.191, 0.13, 0.175, 0.162, 0.135, 0.185, 0.175, 0.169, 0.132, 0.128, 0.134, 0.214, 0.14, 0.179, 0.134, 0.277, 0.049, 0.137, 0.164, 0.124, 0.128, 0.229, 0.137, 0.189, 0.124, 0.223, 0.052, 0.123, 0.168, 0.163, 0.127, 0.126, 0.129, 0.217, 0.123, 0.168, 0.123, 0.132, 0.216, 0.149, 0.193, 0.268, 0.139, 0.138, 0.146, 0.185, 0.169, 0.155, 0.171, 0.186, 0.277, 0.199, 0.184, 0.204, 0.241, 0.348, 0.244, 0.226, 0.223, 0.219, 0.24, 0.253, 0.312, 0.248, 0.29, 0.285, 0.273, 0.27, 0.271, 0.273, 0.282, 0.306, 0.315, 0.321, 0.302, 0.327, 0.296, 0.311, 0.324, 0.333, 0.354, 0.564, 0.346, 0.798, 0.281, 0.431, 0.424, 0.3, 0.359, 0.393, 0.415, 0.203, 0.137, 0.138, 0.137, 0.146, 0.139, 0.21, 0.165, 0.165, 0.164, 0.183, 0.215, 0.191, 0.185, 0.268, 0.183, 0.281]\n",
42
+ "# incremental = [0.14, 0.133, 0.207, 0.146, 0.179, 0.137, 0.132, 0.139, 0.203, 0.194, 0.156, 0.282, 0.172, 0.118, 0.13, 0.386, 0.06, 0.062, 0.062, 0.121, 0.119, 0.125, 0.238, 0.128, 0.166, 0.225, 0.165, 0.166, 0.15, 0.166, 0.149, 0.223, 0.158, 0.179, 0.186, 0.182, 0.187, 0.186, 0.198, 0.197, 0.399, 0.066, 0.132, 0.196, 0.175, 0.123, 0.12, 0.167, 0.127, 0.174, 0.191, 0.127, 0.127, 0.301, 0.056, 0.128, 0.18, 0.179, 0.177, 0.135, 0.157, 0.228, 0.215, 0.215, 0.132, 0.128, 0.173, 0.172, 0.129, 0.137, 0.236, 0.178, 0.144, 0.152, 0.147, 0.158, 0.179, 0.284, 0.177, 0.181, 0.192, 0.181, 0.241, 0.233, 0.231, 0.218, 0.223, 0.221, 0.213, 0.244, 0.244, 0.232, 0.772, 0.058, 0.065, 0.134, 0.175, 0.125, 0.205, 0.132, 0.132, 0.199, 0.197, 0.12, 0.128, 0.168, 0.132, 0.131, 0.288, 0.174, 0.124, 0.195, 0.134, 0.134, 0.265, 0.048, 0.059, 0.073, 0.056, 0.061, 0.059, 0.057, 0.055, 0.057, 0.061, 0.132, 0.138, 0.135, 0.249, 0.132, 0.162, 0.128, 0.121, 0.282, 0.135, 0.161, 0.131, 0.131, 0.189, 0.175, 0.132, 0.135, 0.138, 0.24, 0.169, 0.131, 0.132, 0.143, 0.228, 0.136, 0.141, 0.141, 0.296, 0.052, 0.124, 0.164, 0.128, 0.123, 0.214, 0.199, 0.143, 0.139, 0.265, 0.058, 0.056, 0.131, 0.161, 0.13, 0.127, 0.123, 0.228, 0.135, 0.236, 0.407, 0.25, 0.409, 0.504, 0.336, 0.596, 0.134, 0.143, 0.147, 0.275, 0.315, 0.159, 0.172, 0.182, 0.182, 0.187, 0.191, 0.187, 0.2, 0.242, 0.276, 0.142, 0.123, 0.127, 0.166, 0.175, 0.176, 0.188, 0.211, 0.285, 0.236, 0.297, 0.147, 0.159, 0.154, 0.171, 0.164, 0.228, 0.183, 0.299, 0.395, 0.126, 0.122, 0.171, 0.17, 0.133, 0.152, 0.386, 0.053, 0.134, 0.17, 0.136, 0.131, 0.213, 0.126, 0.195, 0.133, 0.128, 0.13, 0.177, 0.136, 0.14, 0.19, 0.194, 0.162, 0.166, 0.169, 0.227, 0.185, 0.28, 0.188, 0.188, 0.259]\n",
43
+ "# incremental = [1.174, 0.16, 0.2, 0.223, 0.19, 0.147, 0.153, 0.193, 0.137, 0.177, 0.145, 0.258, 0.178, 0.124, 0.124, 0.241, 0.057, 0.056, 0.058, 0.153, 0.147, 0.122, 0.226, 0.126, 0.219, 0.131, 0.139, 0.179, 0.137, 0.142, 0.148, 0.222, 0.151, 0.158, 0.173, 0.186, 0.187, 0.218, 0.176, 0.201, 0.35, 0.051, 0.247, 0.205, 0.134, 0.155, 0.192, 0.132, 0.132, 0.184, 0.14, 0.188, 0.339, 0.049, 0.216, 0.133, 0.118, 0.145, 0.165, 0.126, 0.137, 0.296, 0.205, 0.13, 0.128, 0.236, 0.219, 0.162, 0.135, 0.139, 0.244, 0.191, 0.162, 0.153, 0.156, 0.162, 0.168, 0.212, 0.189, 0.208, 0.177, 0.192, 0.188, 0.199, 0.204, 0.195, 0.224, 0.25, 0.226, 0.256, 0.294, 0.34, 0.207, 0.064, 0.063, 0.125, 0.176, 0.13, 0.208, 0.142, 0.131, 0.189, 0.186, 0.124, 0.122, 0.182, 0.122, 0.29, 0.135, 0.171, 0.13, 0.205, 0.121, 0.132, 0.278, 0.06, 0.056, 0.06, 0.056, 0.053, 0.052, 0.053, 0.055, 0.053, 0.062, 0.13, 0.128, 0.126, 0.239, 0.126, 0.129, 0.136, 0.126, 0.238, 0.133, 0.166, 0.127, 0.144, 0.199, 0.183, 0.14, 0.137, 0.28, 0.132, 0.174, 0.138, 0.123, 0.132, 0.217, 0.172, 0.137, 0.138, 0.295, 0.064, 0.118, 0.166, 0.127, 0.127, 0.175, 0.228, 0.138, 0.125, 0.244, 0.054, 0.059, 0.137, 0.163, 0.121, 0.127, 0.122, 0.234, 0.124, 0.155, 0.183, 0.124, 0.188, 0.166, 0.137, 0.19, 0.306, 0.145, 0.157, 0.301, 0.162, 0.157, 0.163, 0.176, 0.175, 0.177, 0.19, 0.193, 0.203, 0.201, 0.206, 0.214, 0.216, 0.217, 0.237, 0.228, 0.24, 0.234, 0.248, 0.258, 0.268, 0.264, 0.402, 0.29, 0.262, 0.278, 0.282, 0.291, 0.289, 0.316, 0.303, 0.31, 0.306, 0.3, 0.325, 0.327, 0.329, 0.649, 0.06, 0.143, 0.171, 0.127, 0.125, 0.184, 0.125, 0.283, 0.133, 0.131, 0.204, 0.135, 0.142, 0.201, 0.143, 0.16, 0.152, 0.167, 0.219, 0.169, 0.18, 0.287, 0.2, 0.189, 0.277]\n",
44
+ "# synthesize speech\n",
45
+ "# incremental = [0.177, 0.133, 0.192, 0.133, 0.201, 0.133, 0.141, 0.137, 0.175, 0.22, 0.153, 0.331, 0.184, 0.18, 0.147, 0.138, 0.393, 0.067, 0.156, 0.16, 0.136, 0.122, 0.224, 0.205, 0.182, 0.135, 0.271, 0.147, 0.146, 0.178, 0.158, 0.151, 0.164, 0.173, 0.162, 0.186, 0.346, 0.199, 0.12, 0.131, 0.32, 0.064, 0.063, 0.142, 0.165, 0.129, 0.213, 0.124, 0.133, 0.138, 0.284, 0.14, 0.145, 0.374, 0.054, 0.137, 0.164, 0.118, 0.199, 0.133, 0.131, 0.28, 0.163, 0.254, 0.19, 0.119, 0.162, 0.128, 0.132, 0.269, 0.158, 0.235, 0.141, 0.141, 0.242, 0.178, 0.178, 0.151, 0.19, 0.178, 0.236, 0.176, 0.24, 0.206, 0.201, 0.207, 0.213, 0.218, 0.22, 0.257, 0.308, 0.304, 0.076, 0.056, 0.063, 0.069, 0.165, 0.198, 0.151, 0.278, 0.135, 0.21, 0.147, 0.14, 0.229, 0.151, 0.159, 0.151, 0.417, 0.167, 0.124, 0.172, 0.307, 0.144, 0.244, 0.062, 0.054, 0.063, 0.053, 0.06, 0.049, 0.053, 0.075, 0.061, 0.061, 0.054, 0.131, 0.124, 0.124, 0.284, 0.175, 0.138, 0.128, 0.253, 0.13, 0.161, 0.119, 0.151, 0.271, 0.145, 0.217, 0.14, 0.139, 0.269, 0.199, 0.176, 0.135, 0.13, 0.14, 0.257, 0.163, 0.224, 0.144, 0.327, 0.143, 0.183, 0.151, 0.127, 0.136, 0.287, 0.143, 0.13, 0.131, 0.296, 0.052, 0.134, 0.121, 0.169, 0.125, 0.125, 0.272, 0.157, 0.206, 0.199, 0.131, 0.128, 0.176, 0.241, 0.14, 0.244, 0.145, 0.147, 0.352, 0.268, 0.16, 0.163, 0.171, 0.187, 0.202, 0.193, 0.191, 0.23, 0.227, 0.212, 0.285, 0.236, 0.234, 0.217, 0.213, 0.236, 0.238, 0.33, 0.28, 0.268, 0.286, 0.283, 0.286, 0.364, 0.315, 0.29, 0.297, 0.297, 0.297, 0.322, 0.378, 0.311, 0.341, 0.344, 0.334, 0.34, 0.698, 0.069, 0.132, 0.167, 0.186, 0.13, 0.222, 0.136, 0.204, 0.208, 0.131, 0.134, 0.152, 0.145, 0.15, 0.144, 0.26, 0.166, 0.156, 0.162, 0.18, 0.297, 0.185, 0.185, 0.201, 0.328]\n",
46
+ "# incremental = [0.146, 0.133, 0.186, 0.137, 0.199, 0.145, 0.128, 0.163, 0.218, 0.217, 0.159, 0.315, 0.175, 0.176, 0.125, 0.141, 0.277, 0.064, 0.061, 0.123, 0.116, 0.166, 0.217, 0.199, 0.171, 0.211, 0.134, 0.142, 0.148, 0.276, 0.157, 0.156, 0.157, 0.542, 0.19, 0.186, 0.186, 0.187, 0.186, 0.174, 0.432, 0.051, 0.052, 0.127, 0.162, 0.132, 0.125, 0.238, 0.14, 0.134, 0.172, 0.132, 0.135, 0.368, 0.054, 0.246, 0.163, 0.115, 0.166, 0.131, 0.13, 0.28, 0.217, 0.122, 0.126, 0.18, 0.125, 0.134, 0.131, 0.221, 0.134, 0.226, 0.151, 0.144, 0.163, 0.16, 0.169, 0.27, 0.174, 0.184, 0.19, 0.178, 0.187, 0.206, 0.203, 0.202, 0.206, 0.217, 0.224, 0.241, 0.251, 0.233, 0.665, 0.054, 0.052, 0.061, 0.165, 0.173, 0.124, 0.224, 0.135, 0.207, 0.143, 0.197, 0.146, 0.153, 0.147, 0.137, 0.419, 0.166, 0.128, 0.167, 0.237, 0.141, 0.237, 0.056, 0.054, 0.051, 0.054, 0.063, 0.059, 0.059, 0.049, 0.062, 0.056, 0.057, 0.126, 0.116, 0.432, 0.268, 0.121, 0.119, 0.174, 0.266, 0.131, 0.17, 0.127, 0.141, 0.274, 0.161, 0.213, 0.143, 0.158, 0.273, 0.171, 0.176, 0.144, 0.138, 0.217, 0.182, 0.143, 0.203, 0.141, 0.298, 0.129, 0.174, 0.125, 0.124, 0.125, 0.28, 0.135, 0.143, 0.143, 0.3, 0.055, 0.166, 0.165, 0.161, 0.124, 0.129, 0.255, 0.133, 0.178, 0.255, 0.157, 0.421, 0.132, 0.221, 0.245, 0.148, 0.138, 0.231, 0.263, 0.256, 0.449, 0.174, 0.19, 0.189, 0.188, 0.19, 0.194, 0.207, 0.206, 0.206, 0.278, 0.229, 0.221, 0.207, 0.211, 0.238, 0.24, 0.302, 0.264, 0.297, 0.272, 0.264, 0.264, 0.267, 0.269, 0.277, 0.297, 0.299, 0.303, 0.304, 0.301, 0.311, 0.297, 0.342, 0.322, 0.332, 1.311, 0.091, 0.13, 0.15, 0.197, 0.128, 0.217, 0.127, 0.205, 0.207, 0.125, 0.132, 0.139, 0.141, 0.188, 0.184, 0.222, 0.155, 0.154, 0.167, 0.176, 0.298, 0.179, 0.196, 0.185, 0.306]\n",
47
+ "\n",
48
+ "\n",
49
+ "# incremental 2 [_train]\n",
50
+ "# incremental = [0.144, 0.217, 0.149, 0.264, 0.15, 0.241, 0.157, 0.152, 0.25, 0.158, 0.454, 0.233, 0.231, 0.152, 0.148, 0.357, 0.064, 0.06, 0.143, 0.142, 0.17, 0.26, 0.225, 0.237, 0.306, 0.157, 0.164, 0.25, 0.179, 0.294, 0.179, 0.178, 0.192, 0.186, 0.206, 0.198, 0.238, 0.216, 0.217, 0.342, 0.054, 0.056, 0.144, 0.215, 0.146, 0.181, 0.143, 0.15, 0.266, 0.149, 0.143, 0.144, 0.514, 0.049, 0.215, 0.142, 0.222, 0.223, 0.154, 0.164, 0.34, 0.154, 0.241, 0.201, 0.173, 0.297, 0.138, 0.225, 0.27, 0.266, 0.155, 0.154, 0.148, 0.233, 0.166, 0.166, 0.283, 0.178, 0.188, 0.193, 0.193, 0.205, 0.214, 0.203, 0.215, 0.293, 0.242, 0.245, 0.256, 0.259, 0.59, 0.06, 0.058, 0.06, 0.06, 0.151, 0.151, 0.227, 0.261, 0.161, 0.239, 0.164, 0.264, 0.257, 0.156, 0.233, 0.161, 0.506, 0.245, 0.151, 0.225, 0.327, 0.16, 0.337, 0.059, 0.056, 0.056, 0.057, 0.067, 0.058, 0.067, 0.053, 0.052, 0.06, 0.065, 0.148, 0.145, 0.139, 0.38, 0.16, 0.148, 0.149, 0.332, 0.148, 0.232, 0.164, 0.149, 0.301, 0.149, 0.156, 0.159, 0.161, 0.329, 0.225, 0.229, 0.155, 0.16, 0.159, 0.308, 0.151, 0.281, 0.165, 0.43, 0.189, 0.348, 0.138, 0.152, 0.147, 0.267, 0.203, 0.27, 0.228, 0.53, 0.052, 0.147, 0.226, 0.147, 0.15, 0.147, 0.338, 0.145, 0.235, 0.279, 0.154, 0.155, 0.154, 0.351, 0.165, 0.155, 0.162, 0.177, 0.173, 0.248, 0.21, 0.191, 0.206, 0.198, 0.206, 0.211, 0.22, 0.291, 0.312, 0.221, 0.684, 0.236, 0.24, 0.258, 0.256, 0.263, 0.27, 0.277, 0.295, 0.301, 0.345, 0.295, 0.3, 0.309, 0.31, 0.317, 0.319, 0.334, 0.341, 0.372, 0.344, 0.364, 0.369, 0.38, 0.376, 0.398, 0.617, 0.064, 0.141, 0.212, 0.147, 0.153, 0.354, 0.16, 0.221, 0.156, 0.153, 0.163, 0.265, 0.144, 0.236, 0.145, 0.146, 0.175, 0.153, 0.221, 0.325, 0.305, 0.159, 0.179, 0.173, 0.375]\n",
51
+ "\n",
52
+ "# nonincremental infer\n",
53
+ "# incremental = [0.122, 0.196, 0.123, 0.314, 0.136, 0.127, 0.133, 0.204, 0.193, 0.135, 0.306, 0.137, 0.126, 0.174, 0.121, 0.303, 0.066, 0.055, 0.131, 0.135, 0.166, 0.13, 0.245, 0.237, 0.191, 0.135, 0.206, 0.211, 0.149, 0.311, 0.257, 0.233, 0.168, 0.171, 0.178, 0.256, 0.186, 0.196, 0.181, 0.585, 0.06, 0.059, 0.139, 0.169, 0.123, 0.241, 0.202, 0.128, 0.141, 0.226, 0.152, 0.128, 0.392, 0.054, 0.246, 0.166, 0.115, 0.248, 0.139, 0.137, 0.185, 0.243, 0.224, 0.142, 0.185, 0.239, 0.156, 0.17, 0.255, 0.192, 0.243, 0.181, 0.19, 0.223, 0.186, 0.195, 0.304, 0.197, 0.23, 0.316, 0.214, 0.248, 0.236, 0.239, 0.236, 0.328, 0.286, 0.281, 0.262, 0.342, 0.289, 0.796, 0.287, 0.062, 0.052, 0.13, 0.119, 0.165, 0.227, 0.134, 0.24, 0.224, 0.133, 0.15, 0.28, 0.139, 0.143, 0.36, 0.121, 0.462, 0.125, 0.309, 0.145, 0.251, 0.256, 0.06, 0.058, 0.058, 0.056, 0.055, 0.048, 0.047, 0.066, 0.064, 0.069, 0.138, 0.127, 0.134, 0.282, 0.119, 0.123, 0.116, 0.268, 0.128, 0.165, 0.173, 0.141, 0.198, 0.209, 0.129, 0.13, 0.133, 0.328, 0.174, 0.173, 0.13, 0.132, 0.142, 0.252, 0.201, 0.137, 0.136, 0.318, 0.054, 0.123, 0.128, 0.123, 0.118, 0.24, 0.128, 0.202, 0.121, 0.278, 0.058, 0.13, 0.18, 0.124, 0.138, 0.126, 0.271, 0.059, 0.139, 0.164, 0.125, 0.187, 0.261, 0.132, 0.307, 0.132, 0.224, 0.398, 0.303, 0.307, 0.154, 0.153, 0.161, 0.204, 0.196, 0.285, 0.19, 0.203, 0.189, 0.196, 0.366, 0.22, 0.31, 0.258, 0.287, 0.322, 0.236, 0.241, 0.537, 0.311, 0.261, 0.261, 0.402, 0.281, 0.277, 0.28, 0.303, 0.345, 0.307, 0.297, 0.319, 0.487, 0.319, 0.339, 0.33, 0.339, 0.341, 1.037, 0.126, 0.237, 0.21, 0.124, 0.269, 0.161, 0.132, 0.223, 0.143, 0.137, 0.181, 0.143, 0.221, 0.179, 0.263, 0.162, 0.169, 0.254, 0.187, 0.184, 0.321, 0.203, 0.203, 0.444]\n",
54
+ "# incremental = [0.126, 0.202, 0.239, 0.218, 0.152, 0.153, 0.228, 0.137, 0.139, 0.145, 0.349, 0.201, 0.138, 0.165, 0.326, 0.057, 0.056, 0.059, 0.143, 0.131, 0.134, 0.291, 0.15, 0.277, 0.15, 0.197, 0.249, 0.167, 0.26, 0.243, 0.246, 0.156, 0.242, 0.188, 0.26, 0.241, 0.376, 0.209, 0.204, 0.437, 0.054, 0.241, 0.238, 0.139, 0.223, 0.212, 0.193, 0.145, 0.234, 0.212, 0.141, 0.408, 0.045, 0.149, 0.149, 0.134, 0.131, 0.183, 0.145, 0.137, 0.382, 0.305, 0.139, 0.142, 0.249, 0.157, 0.147, 0.265, 0.173, 0.243, 0.192, 0.182, 0.184, 0.189, 0.19, 0.221, 0.303, 0.208, 0.329, 0.226, 0.234, 0.239, 0.247, 0.246, 0.354, 0.39, 0.282, 0.274, 0.274, 0.278, 0.275, 0.823, 0.05, 0.067, 0.141, 0.174, 0.127, 0.274, 0.139, 0.221, 0.143, 0.145, 0.154, 0.298, 0.155, 0.15, 0.407, 0.155, 0.18, 0.132, 0.233, 0.212, 0.143, 0.296, 0.064, 0.089, 0.057, 0.06, 0.054, 0.05, 0.055, 0.056, 0.065, 0.064, 0.169, 0.129, 0.127, 0.311, 0.133, 0.131, 0.134, 0.142, 0.267, 0.132, 0.169, 0.133, 0.13, 0.29, 0.216, 0.147, 0.137, 0.299, 0.14, 0.207, 0.138, 0.141, 0.13, 0.266, 0.286, 0.136, 0.144, 0.339, 0.061, 0.405, 0.182, 0.145, 0.145, 0.182, 0.297, 0.221, 0.148, 0.298, 0.084, 0.07, 0.144, 0.182, 0.14, 0.134, 0.135, 0.288, 0.144, 0.194, 0.216, 0.143, 0.133, 0.135, 0.257, 0.246, 0.25, 0.361, 0.149, 0.422, 0.265, 0.167, 0.171, 0.19, 0.194, 0.182, 0.299, 0.196, 0.342, 0.21, 0.32, 0.222, 0.319, 0.232, 0.235, 0.252, 0.364, 0.249, 0.274, 0.294, 0.315, 0.295, 0.312, 0.285, 0.288, 0.303, 0.285, 0.353, 0.36, 0.314, 0.3, 0.351, 0.317, 0.312, 0.337, 0.561, 0.38, 1.073, 0.057, 0.145, 0.253, 0.136, 0.14, 0.205, 0.156, 0.29, 0.194, 0.136, 0.139, 0.143, 0.22, 0.266, 0.164, 0.159, 0.165, 0.187, 0.26, 0.184, 0.311, 0.236, 0.193, 0.186, 0.464]\n",
55
+ "\n",
56
+ "# incremental infer\n",
57
+ "# incremental = [0.117, 0.179, 0.229, 0.222, 0.143, 0.13, 0.137, 0.192, 0.215, 0.15, 0.257, 0.137, 0.176, 0.139, 0.199, 0.295, 0.066, 0.057, 0.162, 0.129, 0.176, 0.135, 0.266, 0.145, 0.265, 0.184, 0.211, 0.151, 0.221, 0.269, 0.244, 0.237, 0.176, 0.177, 0.181, 0.234, 0.193, 0.185, 0.203, 0.512, 0.057, 0.102, 0.127, 0.19, 0.126, 0.256, 0.227, 0.131, 0.134, 0.134, 0.204, 0.144, 0.35, 0.051, 0.275, 0.166, 0.2, 0.169, 0.131, 0.139, 0.263, 0.228, 0.225, 0.151, 0.178, 0.219, 0.14, 0.176, 0.291, 0.25, 0.213, 0.19, 0.192, 0.199, 0.206, 0.196, 0.28, 0.21, 0.317, 0.265, 0.236, 0.222, 0.272, 0.251, 0.262, 0.361, 0.273, 0.265, 0.272, 0.277, 0.279, 0.732, 0.064, 0.059, 0.066, 0.14, 0.135, 0.248, 0.14, 0.137, 0.198, 0.149, 0.232, 0.145, 0.268, 0.151, 0.151, 0.373, 0.179, 0.176, 0.181, 0.25, 0.131, 0.235, 0.058, 0.058, 0.055, 0.054, 0.059, 0.064, 0.058, 0.061, 0.055, 0.066, 0.057, 0.125, 0.127, 0.134, 0.275, 0.128, 0.126, 0.146, 0.306, 0.129, 0.164, 0.131, 0.128, 0.228, 0.213, 0.133, 0.137, 0.142, 0.319, 0.169, 0.174, 0.134, 0.129, 0.131, 0.302, 0.245, 0.14, 0.144, 0.323, 0.128, 0.164, 0.125, 0.135, 0.132, 0.266, 0.139, 0.191, 0.14, 0.259, 0.053, 0.13, 0.161, 0.122, 0.138, 0.122, 0.263, 0.057, 0.127, 0.158, 0.125, 0.126, 0.191, 0.205, 0.138, 0.129, 0.253, 0.143, 0.151, 0.148, 0.167, 0.165, 0.16, 0.383, 0.174, 0.296, 0.187, 0.334, 0.218, 0.207, 0.364, 0.206, 0.204, 0.226, 0.218, 0.237, 0.235, 0.279, 0.357, 0.273, 0.269, 0.265, 0.274, 0.271, 0.285, 0.293, 0.292, 0.301, 0.308, 0.308, 0.305, 0.302, 0.305, 0.305, 0.336, 0.339, 0.347, 1.08, 0.134, 0.169, 0.19, 0.124, 0.212, 0.178, 0.186, 0.209, 0.134, 0.128, 0.178, 0.137, 0.209, 0.187, 0.225, 0.163, 0.15, 0.161, 0.248, 0.281, 0.221, 0.183, 0.193, 0.429]\n",
58
+ "# incremental = [0.126, 0.189, 0.13, 0.293, 0.133, 0.149, 0.137, 0.235, 0.154, 0.141, 0.326, 0.132, 0.121, 0.17, 0.132, 0.291, 0.058, 0.061, 0.132, 0.132, 0.168, 0.134, 0.249, 0.235, 0.201, 0.144, 0.21, 0.214, 0.145, 0.258, 0.222, 0.223, 0.176, 0.208, 0.227, 0.259, 0.231, 0.32, 0.215, 0.569, 0.057, 0.057, 0.133, 0.129, 0.16, 0.289, 0.203, 0.134, 0.143, 0.23, 0.185, 0.142, 0.364, 0.054, 0.271, 0.165, 0.128, 0.29, 0.196, 0.138, 0.145, 0.198, 0.213, 0.149, 0.189, 0.213, 0.153, 0.159, 0.251, 0.171, 0.281, 0.184, 0.184, 0.189, 0.203, 0.203, 0.285, 0.194, 0.219, 0.347, 0.227, 0.229, 0.246, 0.232, 0.241, 0.365, 0.291, 0.284, 0.275, 0.274, 0.272, 0.779, 0.073, 0.058, 0.055, 0.143, 0.133, 0.159, 0.24, 0.133, 0.133, 0.238, 0.14, 0.144, 0.281, 0.142, 0.145, 0.368, 0.131, 0.165, 0.124, 0.26, 0.128, 0.128, 0.258, 0.061, 0.06, 0.051, 0.063, 0.069, 0.06, 0.055, 0.055, 0.052, 0.065, 0.13, 0.124, 0.124, 0.283, 0.129, 0.127, 0.131, 0.26, 0.056, 0.126, 0.178, 0.142, 0.234, 0.215, 0.13, 0.127, 0.132, 0.31, 0.183, 0.176, 0.133, 0.158, 0.143, 0.251, 0.205, 0.139, 0.144, 0.318, 0.054, 0.128, 0.17, 0.127, 0.129, 0.254, 0.214, 0.198, 0.156, 0.323, 0.057, 0.128, 0.167, 0.168, 0.136, 0.129, 0.25, 0.068, 0.189, 0.177, 0.128, 0.137, 0.283, 0.141, 0.305, 0.14, 0.226, 0.378, 0.263, 0.449, 0.172, 0.158, 0.168, 0.178, 0.221, 0.326, 0.189, 0.188, 0.193, 0.205, 0.371, 0.227, 0.327, 0.216, 0.231, 0.341, 0.251, 0.252, 0.298, 0.261, 0.281, 0.295, 0.359, 0.283, 0.285, 0.294, 0.305, 0.317, 0.322, 0.322, 0.317, 0.471, 0.315, 0.327, 0.328, 0.343, 0.347, 0.962, 0.136, 0.276, 0.237, 0.134, 0.227, 0.137, 0.134, 0.225, 0.151, 0.144, 0.182, 0.165, 0.215, 0.163, 0.243, 0.161, 0.171, 0.177, 0.251, 0.187, 0.315, 0.224, 0.207, 0.426]\n",
59
+ "# incremental = [0.157, 0.136, 0.199, 0.132, 0.266, 0.164, 0.14, 0.148, 0.223, 0.24, 0.148, 0.299, 0.195, 0.184, 0.151, 0.14, 0.284, 0.068, 0.069, 0.181, 0.136, 0.129, 0.226, 0.248, 0.164, 0.358, 0.174, 0.196, 0.154, 0.244, 0.244, 0.224, 0.172, 0.256, 0.187, 0.22, 0.186, 0.219, 0.216, 0.2, 0.528, 0.063, 0.058, 0.139, 0.169, 0.128, 0.136, 0.278, 0.148, 0.132, 0.139, 0.155, 0.149, 0.382, 0.063, 0.144, 0.128, 0.177, 0.181, 0.132, 0.145, 0.317, 0.154, 0.234, 0.148, 0.281, 0.152, 0.164, 0.254, 0.178, 0.252, 0.187, 0.193, 0.184, 0.2, 0.199, 0.207, 0.304, 0.227, 0.313, 0.291, 0.24, 0.249, 0.245, 0.264, 0.333, 0.371, 0.272, 0.278, 0.28, 0.302, 0.756, 0.061, 0.068, 0.056, 0.059, 0.135, 0.163, 0.136, 0.215, 0.256, 0.135, 0.151, 0.227, 0.147, 0.281, 0.17, 0.159, 0.358, 0.166, 0.134, 0.197, 0.311, 0.167, 0.259, 0.061, 0.053, 0.072, 0.07, 0.078, 0.073, 0.078, 0.08, 0.076, 0.074, 0.071, 0.192, 0.126, 0.125, 0.281, 0.134, 0.126, 0.136, 0.25, 0.133, 0.174, 0.139, 0.129, 0.222, 0.216, 0.192, 0.144, 0.135, 0.248, 0.183, 0.176, 0.189, 0.144, 0.22, 0.184, 0.2, 0.135, 0.133, 0.315, 0.128, 0.298, 0.126, 0.126, 0.121, 0.34, 0.143, 0.208, 0.146, 0.276, 0.058, 0.132, 0.135, 0.176, 0.125, 0.127, 0.28, 0.131, 0.165, 0.191, 0.136, 0.128, 0.173, 0.234, 0.289, 0.168, 0.281, 0.415, 0.331, 0.362, 0.161, 0.162, 0.189, 0.194, 0.2, 0.292, 0.2, 0.199, 0.328, 0.363, 0.237, 0.352, 0.218, 0.227, 0.235, 0.291, 0.26, 0.321, 0.273, 0.306, 0.273, 0.274, 0.271, 0.369, 0.293, 0.286, 0.304, 0.308, 0.322, 0.299, 0.313, 0.444, 0.329, 0.35, 0.521, 0.365, 0.908, 0.054, 0.183, 0.167, 0.185, 0.129, 0.222, 0.132, 0.238, 0.139, 0.131, 0.135, 0.185, 0.138, 0.221, 0.187, 0.218, 0.153, 0.158, 0.176, 0.246, 0.297, 0.222, 0.202, 0.191, 0.463]\n",
60
+ "# incremental = [0.329, 0.188, 0.206, 0.228, 0.138, 0.133, 0.138, 0.205, 0.178, 0.143, 0.368, 0.167, 0.167, 0.128, 0.132, 0.283, 0.069, 0.064, 0.13, 0.121, 0.165, 0.174, 0.245, 0.177, 0.253, 0.171, 0.142, 0.208, 0.185, 0.268, 0.23, 0.238, 0.169, 0.167, 0.22, 0.179, 0.19, 0.186, 0.194, 0.512, 0.059, 0.083, 0.123, 0.166, 0.13, 0.247, 0.205, 0.135, 0.144, 0.229, 0.148, 0.139, 0.372, 0.057, 0.127, 0.122, 0.165, 0.208, 0.29, 0.133, 0.215, 0.235, 0.22, 0.146, 0.186, 0.215, 0.161, 0.176, 0.254, 0.168, 0.24, 0.174, 0.181, 0.216, 0.202, 0.201, 0.289, 0.202, 0.294, 0.207, 0.208, 0.214, 0.215, 0.234, 0.245, 0.346, 0.27, 0.271, 0.276, 0.334, 0.272, 0.704, 0.141, 0.052, 0.07, 0.157, 0.13, 0.161, 0.222, 0.137, 0.199, 0.15, 0.217, 0.14, 0.263, 0.149, 0.147, 0.379, 0.162, 0.185, 0.129, 0.237, 0.135, 0.127, 0.228, 0.06, 0.06, 0.064, 0.056, 0.06, 0.057, 0.054, 0.054, 0.061, 0.054, 0.122, 0.144, 0.15, 0.279, 0.13, 0.119, 0.124, 0.254, 0.138, 0.204, 0.181, 0.128, 0.204, 0.183, 0.185, 0.149, 0.148, 0.305, 0.213, 0.174, 0.14, 0.135, 0.143, 0.244, 0.204, 0.134, 0.134, 0.317, 0.06, 0.131, 0.123, 0.128, 0.161, 0.323, 0.128, 0.197, 0.146, 0.268, 0.057, 0.129, 0.169, 0.132, 0.126, 0.165, 0.258, 0.059, 0.174, 0.17, 0.127, 0.128, 0.202, 0.239, 0.243, 0.134, 0.138, 0.136, 0.295, 0.274, 0.155, 0.157, 0.204, 0.304, 0.313, 0.178, 0.205, 0.29, 0.193, 0.198, 0.393, 0.206, 0.307, 0.205, 0.213, 0.309, 0.217, 0.225, 0.266, 0.237, 0.285, 0.32, 0.267, 0.361, 0.28, 0.274, 0.272, 0.286, 0.287, 0.32, 0.308, 0.409, 0.442, 0.561, 0.325, 0.307, 0.315, 0.852, 0.412, 0.161, 0.181, 0.125, 0.216, 0.138, 0.197, 0.191, 0.131, 0.316, 0.173, 0.139, 0.202, 0.151, 0.229, 0.156, 0.157, 0.164, 0.284, 0.282, 0.214, 0.179, 0.185, 0.467]\n",
61
+ "# incremental = [0.189, 0.126, 0.17, 0.23, 0.136, 0.135, 0.164, 0.234, 0.152, 0.141, 0.145, 0.329, 0.138, 0.206, 0.138, 0.325, 0.06, 0.056, 0.061, 0.136, 0.128, 0.13, 0.257, 0.138, 0.245, 0.148, 0.214, 0.214, 0.144, 0.197, 0.278, 0.178, 0.173, 0.257, 0.18, 0.18, 0.225, 0.338, 0.21, 0.2, 0.39, 0.057, 0.143, 0.17, 0.139, 0.242, 0.204, 0.184, 0.136, 0.209, 0.14, 0.148, 0.404, 0.055, 0.162, 0.144, 0.129, 0.172, 0.152, 0.128, 0.133, 0.33, 0.233, 0.139, 0.138, 0.239, 0.149, 0.158, 0.245, 0.172, 0.175, 0.25, 0.177, 0.191, 0.188, 0.195, 0.188, 0.287, 0.202, 0.301, 0.22, 0.222, 0.229, 0.229, 0.24, 0.275, 0.34, 0.279, 0.267, 0.27, 0.276, 0.291, 0.748, 0.058, 0.059, 0.142, 0.172, 0.152, 0.25, 0.136, 0.214, 0.142, 0.157, 0.138, 0.266, 0.152, 0.148, 0.146, 0.368, 0.172, 0.13, 0.233, 0.14, 0.135, 0.28, 0.058, 0.059, 0.06, 0.097, 0.064, 0.058, 0.058, 0.057, 0.055, 0.059, 0.16, 0.126, 0.128, 0.282, 0.138, 0.125, 0.122, 0.132, 0.322, 0.128, 0.199, 0.431, 0.121, 0.204, 0.189, 0.13, 0.124, 0.264, 0.135, 0.173, 0.147, 0.125, 0.127, 0.244, 0.193, 0.126, 0.132, 0.294, 0.051, 0.133, 0.159, 0.127, 0.124, 0.207, 0.221, 0.195, 0.138, 0.258, 0.048, 0.058, 0.128, 0.164, 0.126, 0.122, 0.135, 0.272, 0.13, 0.166, 0.186, 0.124, 0.173, 0.133, 0.239, 0.269, 0.135, 0.284, 0.293, 0.244, 0.227, 0.162, 0.157, 0.194, 0.185, 0.203, 0.284, 0.202, 0.33, 0.212, 0.314, 0.216, 0.395, 0.242, 0.235, 0.235, 0.339, 0.253, 0.331, 0.287, 0.284, 0.277, 0.313, 0.281, 0.318, 0.3, 0.358, 0.317, 0.31, 0.309, 0.306, 0.462, 0.338, 0.334, 0.354, 0.479, 0.373, 0.911, 0.055, 0.185, 0.161, 0.127, 0.24, 0.125, 0.129, 0.215, 0.179, 0.127, 0.143, 0.135, 0.208, 0.223, 0.162, 0.15, 0.161, 0.18, 0.238, 0.185, 0.309, 0.217, 0.207, 0.19, 0.425]\n",
62
+ "# incremental = [0.133, 0.192, 0.198, 0.229, 0.133, 0.135, 0.239, 0.146, 0.143, 0.141, 0.325, 0.186, 0.164, 0.146, 0.304, 0.059, 0.058, 0.052, 0.13, 0.14, 0.139, 0.272, 0.167, 0.308, 0.152, 0.144, 0.243, 0.149, 0.211, 0.223, 0.227, 0.163, 0.28, 0.184, 0.178, 0.243, 0.338, 0.199, 0.226, 0.391, 0.06, 0.19, 0.207, 0.129, 0.21, 0.197, 0.169, 0.133, 0.132, 0.231, 0.145, 0.361, 0.055, 0.189, 0.135, 0.125, 0.123, 0.166, 0.133, 0.129, 0.319, 0.225, 0.139, 0.151, 0.19, 0.243, 0.152, 0.256, 0.165, 0.25, 0.191, 0.173, 0.204, 0.21, 0.212, 0.198, 0.296, 0.197, 0.312, 0.234, 0.225, 0.217, 0.259, 0.249, 0.336, 0.348, 0.289, 0.274, 0.308, 0.273, 0.29, 0.742, 0.066, 0.055, 0.128, 0.16, 0.132, 0.238, 0.134, 0.221, 0.142, 0.135, 0.149, 0.268, 0.151, 0.147, 0.152, 0.361, 0.162, 0.124, 0.177, 0.126, 0.127, 0.303, 0.061, 0.061, 0.053, 0.053, 0.057, 0.059, 0.056, 0.063, 0.059, 0.079, 0.187, 0.126, 0.132, 0.279, 0.131, 0.175, 0.123, 0.135, 0.304, 0.127, 0.155, 0.143, 0.126, 0.215, 0.197, 0.177, 0.133, 0.245, 0.125, 0.168, 0.168, 0.122, 0.122, 0.257, 0.223, 0.142, 0.129, 0.311, 0.073, 0.136, 0.203, 0.135, 0.125, 0.752, 0.382, 0.217, 0.13, 0.262, 0.056, 0.057, 0.135, 0.181, 0.128, 0.132, 0.126, 0.249, 0.126, 0.161, 0.204, 0.132, 0.133, 0.133, 0.243, 0.247, 0.213, 0.359, 0.148, 0.433, 0.243, 0.152, 0.176, 0.186, 0.187, 0.178, 0.286, 0.199, 0.201, 0.198, 0.367, 0.32, 0.198, 0.224, 0.218, 0.216, 0.341, 0.233, 0.241, 0.291, 0.311, 0.286, 0.31, 0.28, 0.277, 0.274, 0.288, 0.361, 0.31, 0.303, 0.291, 0.363, 0.425, 0.318, 0.362, 0.463, 0.365, 0.918, 0.062, 0.194, 0.164, 0.129, 0.126, 0.19, 0.134, 0.279, 0.186, 0.139, 0.137, 0.144, 0.228, 0.233, 0.15, 0.155, 0.162, 0.166, 0.272, 0.187, 0.323, 0.183, 0.188, 0.198, 0.423]\n",
63
+ "# incremental = [0.187, 0.13, 0.26, 0.14, 0.134, 0.251, 0.132, 0.212, 0.15, 0.143, 0.363, 0.141, 0.167, 0.128, 0.285, 0.055, 0.052, 0.136, 0.13, 0.168, 0.129, 0.247, 0.241, 0.194, 0.152, 0.218, 0.209, 0.151, 0.267, 0.175, 0.224, 0.165, 0.174, 0.269, 0.201, 0.243, 0.322, 0.219, 0.199, 0.558, 0.058, 0.139, 0.171, 0.14, 0.336, 0.141, 0.137, 0.148, 0.24, 0.319, 0.149, 0.56, 0.055, 0.137, 0.172, 0.134, 0.269, 0.129, 0.138, 0.131, 0.245, 0.22, 0.137, 0.183, 0.232, 0.16, 0.151, 0.247, 0.171, 0.253, 0.19, 0.182, 0.189, 0.204, 0.186, 0.199, 0.328, 0.232, 0.372, 0.229, 0.214, 0.228, 0.242, 0.234, 0.653, 0.289, 0.324, 0.344, 0.293, 0.279, 0.749, 0.063, 0.056, 0.057, 0.136, 0.124, 0.167, 0.251, 0.139, 0.134, 0.236, 0.134, 0.147, 0.322, 0.162, 0.142, 0.376, 0.14, 0.173, 0.138, 0.266, 0.142, 0.136, 0.278, 0.065, 0.059, 0.059, 0.062, 0.057, 0.055, 0.063, 0.064, 0.072, 0.065, 0.136, 0.124, 0.12, 0.276, 0.132, 0.164, 0.138, 0.256, 0.05, 0.134, 0.167, 0.124, 0.234, 0.152, 0.201, 0.154, 0.137, 0.334, 0.174, 0.183, 0.132, 0.134, 0.167, 0.308, 0.209, 0.143, 0.141, 0.313, 0.056, 0.13, 0.176, 0.131, 0.133, 0.245, 0.214, 0.206, 0.148, 0.25, 0.053, 0.122, 0.118, 0.189, 0.13, 0.128, 0.136, 0.24, 0.135, 0.171, 0.126, 0.146, 0.216, 0.208, 0.29, 0.133, 0.132, 0.286, 0.336, 0.263, 0.157, 0.166, 0.173, 0.184, 0.185, 0.284, 0.185, 0.203, 0.203, 0.198, 0.359, 0.312, 0.339, 0.256, 0.253, 0.284, 0.335, 0.259, 0.334, 0.307, 0.304, 0.281, 0.373, 0.3, 0.289, 0.29, 0.286, 0.327, 0.3, 0.299, 0.32, 0.435, 0.32, 0.464, 0.339, 0.334, 0.348, 1.288, 0.058, 0.201, 0.126, 0.527, 0.284, 0.148, 0.13, 0.21, 0.175, 0.138, 0.135, 0.141, 0.215, 0.238, 0.16, 0.164, 0.161, 0.186, 0.247, 0.192, 0.375, 0.191, 0.2, 0.431]\n",
64
+ "incremental = [0.153, 0.171, 0.197, 0.138, 0.23, 0.137, 0.141, 0.145, 0.225, 0.146, 0.148, 0.336, 0.138, 0.163, 0.181, 0.133, 0.354, 0.064, 0.053, 0.133, 0.132, 0.166, 0.135, 0.232, 0.234, 0.231, 0.204, 0.214, 0.157, 0.189, 0.236, 0.221, 0.245, 0.229, 0.205, 0.203, 0.248, 0.194, 0.201, 0.197, 0.483, 0.057, 0.052, 0.132, 0.158, 0.13, 0.251, 0.197, 0.135, 0.143, 0.262, 0.144, 0.135, 0.38, 0.054, 0.226, 0.128, 0.166, 0.261, 0.139, 0.132, 0.284, 0.23, 0.234, 0.142, 0.211, 0.217, 0.155, 0.163, 0.285, 0.169, 0.242, 0.201, 0.188, 0.189, 0.186, 0.187, 0.311, 0.214, 0.213, 0.304, 0.23, 0.265, 0.219, 0.234, 0.252, 0.239, 0.339, 0.278, 0.365, 0.271, 0.277, 0.729, 0.051, 0.055, 0.059, 0.132, 0.121, 0.162, 0.239, 0.133, 0.135, 0.214, 0.142, 0.132, 0.258, 0.144, 0.135, 0.386, 0.13, 0.135, 0.133, 0.273, 0.136, 0.133, 0.254, 0.05, 0.049, 0.05, 0.053, 0.093, 0.064, 0.055, 0.053, 0.049, 0.056, 0.142, 0.125, 0.131, 0.273, 0.131, 0.124, 0.121, 0.264, 0.135, 0.172, 0.159, 0.136, 0.205, 0.21, 0.159, 0.159, 0.14, 0.324, 0.161, 0.171, 0.127, 0.139, 0.154, 0.293, 0.22, 0.149, 0.188, 0.331, 0.051, 0.133, 0.132, 0.122, 0.152, 0.25, 0.191, 0.277, 0.134, 0.259, 0.051, 0.129, 0.168, 0.169, 0.134, 0.121, 0.253, 0.051, 0.147, 0.163, 0.137, 0.13, 0.264, 0.286, 0.179, 0.142, 0.224, 0.367, 0.156, 0.444, 0.173, 0.178, 0.187, 0.186, 0.189, 0.277, 0.184, 0.189, 0.189, 0.198, 0.366, 0.214, 0.306, 0.221, 0.221, 0.335, 0.269, 0.253, 0.299, 0.285, 0.28, 0.271, 0.353, 0.285, 0.291, 0.28, 0.307, 0.297, 0.31, 0.297, 0.305, 0.48, 0.323, 0.33, 0.329, 0.33, 0.333, 0.922, 0.131, 0.168, 0.188, 0.135, 0.21, 0.143, 0.13, 0.224, 0.138, 0.147, 0.198, 0.14, 0.223, 0.156, 0.239, 0.164, 0.163, 0.295, 0.184, 0.186, 0.333, 0.204, 0.197, 0.439]\n",
65
+ "# self_attn_state\n",
66
+ "# incremental = [0.122, 0.2, 0.133, 0.321, 0.136, 0.154, 0.141, 0.181, 0.212, 0.143, 0.313, 0.172, 0.134, 0.136, 0.356, 0.057, 0.078, 0.056, 0.139, 0.121, 0.158, 0.218, 0.203, 0.171, 0.208, 0.142, 0.24, 0.14, 0.141, 0.142, 0.158, 0.155, 0.184, 0.181, 0.221, 0.459, 0.198, 0.123, 0.126, 0.27, 0.067, 0.125, 0.163, 0.188, 0.129, 0.125, 0.239, 0.138, 0.136, 0.138, 0.133, 0.137, 0.381, 0.055, 0.129, 0.118, 0.166, 0.171, 0.131, 0.134, 0.278, 0.187, 0.229, 0.182, 0.129, 0.172, 0.128, 0.126, 0.248, 0.144, 0.217, 0.148, 0.14, 0.151, 0.266, 0.502, 0.155, 0.161, 0.211, 0.178, 0.176, 0.186, 0.187, 0.183, 0.211, 0.204, 0.22, 0.214, 0.206, 0.296, 0.252, 0.192, 0.063, 0.054, 0.056, 0.128, 0.124, 0.171, 0.263, 0.134, 0.198, 0.145, 0.26, 0.155, 0.123, 0.197, 0.127, 0.413, 0.209, 0.123, 0.194, 0.131, 0.128, 0.297, 0.053, 0.05, 0.057, 0.055, 0.046, 0.063, 0.06, 0.057, 0.057, 0.059, 0.135, 0.164, 0.121, 0.116, 0.258, 0.124, 0.118, 0.127, 0.285, 0.131, 0.164, 0.128, 0.134, 0.223, 0.208, 0.148, 0.124, 0.127, 0.263, 0.166, 0.128, 0.139, 0.162, 0.296, 0.189, 0.13, 0.135, 0.318, 0.049, 0.123, 0.17, 0.128, 0.125, 0.21, 0.214, 0.139, 0.14, 0.274, 0.051, 0.062, 0.131, 0.145, 0.162, 0.136, 0.125, 0.282, 0.13, 0.164, 0.205, 0.129, 0.156, 0.156, 0.251, 0.357, 0.139, 0.138, 0.149, 0.353, 0.19, 0.304, 0.165, 0.186, 0.194, 0.196, 0.183, 0.187, 0.194, 0.199, 0.206, 0.267, 0.212, 0.257, 0.228, 0.226, 0.225, 0.239, 0.3, 0.268, 0.272, 0.278, 0.303, 0.283, 0.274, 0.279, 0.274, 0.289, 0.303, 0.3, 0.303, 0.313, 0.305, 0.332, 0.344, 0.343, 0.342, 1.678, 0.062, 0.164, 0.13, 0.204, 0.171, 0.217, 0.171, 0.247, 0.207, 0.134, 0.13, 0.138, 0.138, 0.147, 0.187, 0.142, 0.242, 0.158, 0.16, 0.168, 0.214, 0.284, 0.185, 0.179, 0.365]\n",
67
+ "# incremental = [0.231, 0.134, 0.238, 0.195, 0.206, 0.147, 0.136, 0.149, 0.187, 0.214, 0.148, 0.354, 0.19, 0.213, 0.139, 0.138, 0.281, 0.061, 0.069, 0.178, 0.145, 0.133, 0.21, 0.208, 0.14, 0.287, 0.178, 0.173, 0.149, 0.27, 0.398, 0.376, 0.496, 0.164, 0.162, 0.178, 0.375, 0.273, 0.123, 0.126, 0.327, 0.058, 0.063, 0.134, 0.167, 0.127, 0.19, 0.167, 0.128, 0.132, 0.269, 0.14, 0.136, 0.388, 0.052, 0.245, 0.169, 0.126, 0.163, 0.126, 0.133, 0.264, 0.133, 0.246, 0.153, 0.164, 0.158, 0.119, 0.129, 0.254, 0.157, 0.268, 0.14, 0.135, 0.241, 0.143, 0.151, 0.155, 0.161, 0.188, 0.233, 0.191, 0.184, 0.179, 0.224, 0.213, 0.218, 0.226, 0.21, 0.211, 0.297, 0.253, 0.048, 0.056, 0.06, 0.059, 0.12, 0.169, 0.13, 0.233, 0.136, 0.21, 0.173, 0.162, 0.263, 0.146, 0.153, 0.139, 0.406, 0.191, 0.144, 0.163, 0.267, 0.139, 0.298, 0.055, 0.061, 0.049, 0.052, 0.055, 0.053, 0.051, 0.06, 0.053, 0.048, 0.052, 0.166, 0.123, 0.123, 0.26, 0.131, 0.161, 0.124, 0.277, 0.124, 0.159, 0.131, 0.131, 0.211, 0.135, 0.2, 0.135, 0.174, 0.285, 0.186, 0.179, 0.139, 0.133, 0.231, 0.181, 0.137, 0.199, 0.135, 0.306, 0.128, 0.173, 0.119, 0.116, 0.126, 0.285, 0.151, 0.136, 0.151, 0.363, 0.063, 0.173, 0.129, 0.169, 0.121, 0.129, 0.326, 0.13, 0.168, 0.212, 0.125, 0.136, 0.132, 0.247, 0.313, 0.144, 0.142, 0.149, 0.293, 0.178, 0.363, 0.176, 0.188, 0.185, 0.192, 0.178, 0.192, 0.213, 0.199, 0.204, 0.256, 0.21, 0.22, 0.208, 0.226, 0.242, 0.241, 0.556, 0.257, 0.262, 0.259, 0.26, 0.271, 0.379, 0.318, 0.282, 0.304, 0.331, 0.322, 0.295, 0.328, 0.328, 0.301, 0.345, 0.331, 0.333, 0.69, 0.056, 0.123, 0.167, 0.196, 0.127, 0.21, 0.129, 0.208, 0.234, 0.134, 0.141, 0.144, 0.137, 0.142, 0.139, 0.242, 0.152, 0.146, 0.16, 0.18, 0.287, 0.184, 0.18, 0.177, 0.321]\n",
68
+ "fig, ax = plt.subplots()\n",
69
+ "ax.boxplot([nonincremental, incremental])\n",
70
+ "ax.set_xticklabels([\"nonincremental\", \"incremental\"])"
71
+ ]
72
+ },
73
+ {
74
+ "cell_type": "code",
75
+ "execution_count": 12,
76
+ "metadata": {},
77
+ "outputs": [
78
+ {
79
+ "name": "stdout",
80
+ "output_type": "stream",
81
+ "text": [
82
+ "0.262 0.20222357723577236\n",
83
+ "0.2530612244897959 0.0975609756097561\n"
84
+ ]
85
+ }
86
+ ],
87
+ "source": [
88
+ "print(np.mean(nonincremental), np.mean(incremental))\n",
89
+ "print(sum([1 for x in nonincremental if x > 0.32])/len(nonincremental), sum([1 for x in incremental if x > 0.32])/len(incremental))"
90
+ ]
91
+ },
92
+ {
93
+ "cell_type": "code",
94
+ "execution_count": null,
95
+ "metadata": {},
96
+ "outputs": [],
97
+ "source": []
98
+ },
99
+ {
100
+ "cell_type": "code",
101
+ "execution_count": 75,
102
+ "metadata": {},
103
+ "outputs": [
104
+ {
105
+ "data": {
106
+ "text/plain": [
107
+ "0.41239999999999993"
108
+ ]
109
+ },
110
+ "execution_count": 75,
111
+ "metadata": {},
112
+ "output_type": "execute_result"
113
+ }
114
+ ],
115
+ "source": [
116
+ "np.percentile(nonincremental, 90)"
117
+ ]
118
+ },
119
+ {
120
+ "cell_type": "code",
121
+ "execution_count": 23,
122
+ "metadata": {},
123
+ "outputs": [
124
+ {
125
+ "data": {
126
+ "text/plain": [
127
+ "0.3105"
128
+ ]
129
+ },
130
+ "execution_count": 23,
131
+ "metadata": {},
132
+ "output_type": "execute_result"
133
+ }
134
+ ],
135
+ "source": [
136
+ "np.percentile(incremental, 90)"
137
+ ]
138
+ },
139
+ {
140
+ "cell_type": "code",
141
+ "execution_count": null,
142
+ "metadata": {},
143
+ "outputs": [],
144
+ "source": []
145
+ }
146
+ ],
147
+ "metadata": {
148
+ "kernelspec": {
149
+ "display_name": "fairseq-20230220-seamless",
150
+ "language": "python",
151
+ "name": "python3"
152
+ },
153
+ "language_info": {
154
+ "codemirror_mode": {
155
+ "name": "ipython",
156
+ "version": 3
157
+ },
158
+ "file_extension": ".py",
159
+ "mimetype": "text/x-python",
160
+ "name": "python",
161
+ "nbconvert_exporter": "python",
162
+ "pygments_lexer": "ipython3",
163
+ "version": "3.9.16"
164
+ },
165
+ "orig_nbformat": 4
166
+ },
167
+ "nbformat": 4,
168
+ "nbformat_minor": 2
169
+ }
seamless_server/src/transcoder_helpers.py ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+
3
+ logger = logging.getLogger("socketio_server_pubsub")
4
+
5
+
6
+ def get_transcoder_output_events(transcoder) -> list:
7
+ speech_and_text_output = transcoder.get_buffered_output()
8
+ if speech_and_text_output is None:
9
+ logger.debug("No output from transcoder.get_buffered_output()")
10
+ return []
11
+
12
+ logger.debug(f"We DID get output from the transcoder! {speech_and_text_output}")
13
+
14
+ lat = None
15
+
16
+ events = []
17
+
18
+ if speech_and_text_output.speech_samples:
19
+ events.append(
20
+ {
21
+ "event": "translation_speech",
22
+ "payload": speech_and_text_output.speech_samples,
23
+ "sample_rate": speech_and_text_output.speech_sample_rate,
24
+ }
25
+ )
26
+
27
+ if speech_and_text_output.text:
28
+ events.append(
29
+ {
30
+ "event": "translation_text",
31
+ "payload": speech_and_text_output.text,
32
+ }
33
+ )
34
+
35
+ for e in events:
36
+ e["eos"] = speech_and_text_output.final
37
+
38
+ # if not latency_sent:
39
+ # lat = transcoder.first_translation_time()
40
+ # latency_sent = True
41
+ # to_send["latency"] = lat
42
+
43
+ return events
streaming-react-app/.eslintrc.cjs ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ module.exports = {
2
+ root: true,
3
+ env: {browser: true, es2020: true},
4
+ extends: [
5
+ 'eslint:recommended',
6
+ 'plugin:@typescript-eslint/recommended',
7
+ 'plugin:react-hooks/recommended',
8
+ ],
9
+ ignorePatterns: ['dist', '.eslintrc.cjs'],
10
+ parser: '@typescript-eslint/parser',
11
+ plugins: ['react-refresh'],
12
+ rules: {
13
+ 'react-refresh/only-export-components': [
14
+ 'warn',
15
+ {allowConstantExport: true},
16
+ ],
17
+ },
18
+ };
streaming-react-app/.gitignore ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Logs
2
+ logs
3
+ *.log
4
+ npm-debug.log*
5
+ yarn-debug.log*
6
+ yarn-error.log*
7
+ pnpm-debug.log*
8
+ lerna-debug.log*
9
+
10
+ node_modules
11
+ dist
12
+ dist-ssr
13
+ *.local
14
+
15
+ # Editor directories and files
16
+ .vscode/*
17
+ !.vscode/extensions.json
18
+ .idea
19
+ .DS_Store
20
+ *.suo
21
+ *.ntvs*
22
+ *.njsproj
23
+ *.sln
24
+ *.sw?
streaming-react-app/README.md ADDED
@@ -0,0 +1,65 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 🚀 Streaming React App
2
+
3
+ ## Getting Started
4
+
5
+ - `yarn run dev` - Run the app with a development server that supports hot module reloading
6
+
7
+ ## URL Parameters
8
+
9
+ You can provide URL parameters in order to change the behavior of the app. Those are documented in [URLParams.ts](src/URLParams.ts).
10
+
11
+ # Vite Information: React + TypeScript + Vite
12
+
13
+ This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
14
+
15
+ Currently, two official plugins are available:
16
+
17
+ - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
18
+ - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
19
+
20
+ ## Expanding the ESLint configuration
21
+
22
+ If you are developing a production application, we recommend updating the configuration to enable type aware lint rules:
23
+
24
+ - Configure the top-level `parserOptions` property like this:
25
+
26
+ ```js
27
+ parserOptions: {
28
+ ecmaVersion: 'latest',
29
+ sourceType: 'module',
30
+ project: ['./tsconfig.json', './tsconfig.node.json'],
31
+ tsconfigRootDir: __dirname,
32
+ },
33
+ ```
34
+
35
+ - Replace `plugin:@typescript-eslint/recommended` to `plugin:@typescript-eslint/recommended-type-checked` or `plugin:@typescript-eslint/strict-type-checked`
36
+ - Optionally add `plugin:@typescript-eslint/stylistic-type-checked`
37
+ - Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and add `plugin:react/recommended` & `plugin:react/jsx-runtime` to the `extends` list
38
+
39
+ # To Deploy to AWS
40
+
41
+ 1. Acquire AWS credentials (not needed if already on an EC2 instance with permissions)
42
+
43
+ On your local mac use the following command.
44
+
45
+ ```
46
+ eval $(corp_cloud aws get-creds 790537050551)
47
+ ```
48
+
49
+ 2. Deploy to AWS
50
+
51
+ Build the react and copy the contents of [dist](dist) folder to s3 bucket and then invalidate the cloudfront (CDN) cache. Note step 2 has been automated using `yarn deploy_dev`
52
+
53
+ To deploy to the (old) seamless-vc s3 bucket:
54
+
55
+ ```
56
+ yarn build:dev_vc
57
+ yarn deploy_dev_vc
58
+ ```
59
+
60
+ To deploy to the (new) seamless-vr terraform-based s3 bucket:
61
+
62
+ ```
63
+ yarn build:dev_vr
64
+ yarn deploy_dev_vr
65
+ ```
streaming-react-app/index.html ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <link rel="icon" type="image/svg+xml" href="/src/assets/seamless.svg" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+ <title>Seamless Translation</title>
8
+ </head>
9
+ <body>
10
+ <div id="root"></div>
11
+ <script type="module" src="/src/main.tsx"></script>
12
+ </body>
13
+ </html>
streaming-react-app/package-lock.json ADDED
The diff for this file is too large to render. See raw diff
 
streaming-react-app/package.json ADDED
@@ -0,0 +1,58 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "streaming-react-app",
3
+ "private": true,
4
+ "version": "0.0.12",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite --host --strictPort",
8
+ "build": "vite build",
9
+ "build:dev_vr": "yarn build --mode deploy_dev_vr",
10
+ "build:dev_vc": "yarn build --mode deploy_dev_vc",
11
+ "preview": "vite preview",
12
+ "clean:node-modules": "rm -rf node_modules/",
13
+ "ts-check": "tsc --noEmit",
14
+ "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
15
+ "prettier-check": "cd ../ && yarn run prettier-base --check streaming-react-app",
16
+ "signal": "concurrently --names \"TS,LINT,PRETTIER\" -c \"bgBlack.bold,bgRed.bold,bgCyan.bold\" \"yarn run ts-check\" \"yarn run lint\" \"yarn run prettier-check\"",
17
+ "deploy_dev_vr": "aws s3 sync dist/ s3://dev-seamless-vr-www && aws cloudfront create-invalidation --distribution-id E38ZTD4R79FGYF --paths \"/*\"",
18
+ "deploy_dev_vc": "aws s3 sync dist/ s3://seamless-vc.dev.metademolab.com && aws cloudfront create-invalidation --distribution-id E29D1W2ORBP77G --paths \"/*\""
19
+ },
20
+ "dependencies": {
21
+ "@emotion/react": "11.11.1",
22
+ "@emotion/styled": "11.11.0",
23
+ "@mui/icons-material": "5.14.3",
24
+ "@mui/material": "5.14.5",
25
+ "@react-three/drei": "^9.83.9",
26
+ "@react-three/fiber": "^8.14.1",
27
+ "@react-three/xr": "^5.7.1",
28
+ "amazon-cognito-identity-js": "^6.3.6",
29
+ "audiobuffer-to-wav": "^1.0.0",
30
+ "aws-sdk": "^2.1472.0",
31
+ "iso-639-1": "^3.1.0",
32
+ "js-cookie": "^3.0.5",
33
+ "lodash": "4.17.21",
34
+ "react": "^18.2.0",
35
+ "react-dom": "^18.2.0",
36
+ "react-google-charts": "^4.0.1",
37
+ "socket.io-client": "^4.7.2",
38
+ "three": "^0.156.1",
39
+ "three-mesh-ui": "^6.5.4",
40
+ "uuid": "^9.0.0",
41
+ "zustand": "^4.4.3"
42
+ },
43
+ "devDependencies": {
44
+ "@types/node": "^20.5.3",
45
+ "@types/react": "^18.2.15",
46
+ "@types/react-dom": "^18.2.7",
47
+ "@types/uuid": "^9.0.2",
48
+ "@typescript-eslint/eslint-plugin": "^6.0.0",
49
+ "@typescript-eslint/parser": "^6.0.0",
50
+ "@vitejs/plugin-react": "^4.0.3",
51
+ "concurrently": "8.2.1",
52
+ "eslint": "^8.45.0",
53
+ "eslint-plugin-react-hooks": "^4.6.0",
54
+ "eslint-plugin-react-refresh": "^0.4.3",
55
+ "typescript": "5.1.6",
56
+ "vite": "^4.4.5"
57
+ }
58
+ }
streaming-react-app/public/vite.svg ADDED
streaming-react-app/src/App.tsx ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import SocketWrapper from './SocketWrapper';
2
+ import {ThemeProvider} from '@mui/material/styles';
3
+ import theme from './theme';
4
+ import StreamingInterface from './StreamingInterface';
5
+ import CssBaseline from '@mui/material/CssBaseline';
6
+ import {createContext, useCallback, useState} from 'react';
7
+ import packageJson from '../package.json';
8
+
9
+ console.log(`Streaming React App version: ${packageJson?.version}`);
10
+
11
+ // Roboto font for mui ui library
12
+ // import '@fontsource/roboto/300.css';
13
+ // import '@fontsource/roboto/400.css';
14
+ // import '@fontsource/roboto/500.css';
15
+ // import '@fontsource/roboto/700.css';
16
+
17
+ export const AppResetKeyContext = createContext<(newKey: string) => void>(
18
+ () => {
19
+ throw new Error('AppResetKeyContext not initialized');
20
+ },
21
+ );
22
+
23
+ function App() {
24
+ return (
25
+ <ThemeProvider theme={theme}>
26
+ <CssBaseline />
27
+ <SocketWrapper>
28
+ <StreamingInterface />
29
+ </SocketWrapper>
30
+ </ThemeProvider>
31
+ );
32
+ }
33
+
34
+ function AppWrapper() {
35
+ const [appResetKey, setAppResetKey] = useState<string>('[initial value]');
36
+ const setAppResetKeyHandler = useCallback((newKey: string) => {
37
+ setAppResetKey((prev) => {
38
+ console.warn(
39
+ `Resetting the app with appResetKey: ${newKey}; prevKey: ${prev}`,
40
+ );
41
+ if (prev === newKey) {
42
+ console.error(
43
+ `The appResetKey was the same as the previous key, so the app will not reset.`,
44
+ );
45
+ }
46
+ return newKey;
47
+ });
48
+ }, []);
49
+
50
+ return (
51
+ <AppResetKeyContext.Provider value={setAppResetKeyHandler}>
52
+ <App key={appResetKey} />
53
+ </AppResetKeyContext.Provider>
54
+ );
55
+ }
56
+
57
+ export default AppWrapper;
streaming-react-app/src/Blink.tsx ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import Box from '@mui/material/Box';
2
+ import {useEffect, useState} from 'react';
3
+
4
+ type Props = {
5
+ intervalMs: number;
6
+ children: React.ReactNode;
7
+ shouldBlink: boolean;
8
+ // display?: 'block' | 'inline' | 'inline-block';
9
+ };
10
+
11
+ export default function Blink({
12
+ // display = 'inline-block',
13
+ shouldBlink,
14
+ intervalMs,
15
+ children,
16
+ }: Props): React.ReactElement {
17
+ const [cursorBlinkOn, setCursorBlinkOn] = useState(false);
18
+
19
+ useEffect(() => {
20
+ if (shouldBlink) {
21
+ const interval = setInterval(() => {
22
+ setCursorBlinkOn((prev) => !prev);
23
+ }, intervalMs);
24
+
25
+ return () => clearInterval(interval);
26
+ } else {
27
+ setCursorBlinkOn(false);
28
+ }
29
+ }, [intervalMs, shouldBlink]);
30
+
31
+ return (
32
+ <Box
33
+ component="span"
34
+ sx={{
35
+ display: 'inline-block',
36
+ visibility: cursorBlinkOn ? 'visible' : 'hidden',
37
+ }}>
38
+ {children}
39
+ </Box>
40
+ );
41
+ }
streaming-react-app/src/DebugSection.tsx ADDED
@@ -0,0 +1,62 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import {Chart} from 'react-google-charts';
2
+ import debug from './debug';
3
+ import {
4
+ Accordion,
5
+ AccordionDetails,
6
+ AccordionSummary,
7
+ Button,
8
+ Typography,
9
+ } from '@mui/material';
10
+ import {useState} from 'react';
11
+ import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown';
12
+
13
+ export default function DebugChart() {
14
+ const [showDebugTimings, setShowDebugTimings] = useState<boolean>(false);
15
+
16
+ const data = debug()?.getChartData();
17
+ const options = {
18
+ timeline: {
19
+ groupByRowLabel: true,
20
+ },
21
+ };
22
+
23
+ return (
24
+ <div className="horizontal-padding-sra text-chunk-sra">
25
+ <Accordion
26
+ expanded={showDebugTimings}
27
+ onChange={() => setShowDebugTimings(!showDebugTimings)}
28
+ elevation={0}
29
+ sx={{border: 1, borderColor: 'rgba(0, 0, 0, 0.3)'}}>
30
+ <AccordionSummary
31
+ expandIcon={<ArrowDropDownIcon />}
32
+ className="debug-section">
33
+ Debug Info
34
+ </AccordionSummary>
35
+ <AccordionDetails>
36
+ {data && data.length > 1 ? (
37
+ <>
38
+ <Chart
39
+ chartType="Timeline"
40
+ data={data}
41
+ width="100%"
42
+ height="400px"
43
+ options={options}
44
+ />
45
+ <Button
46
+ variant="contained"
47
+ sx={{marginBottom: 1}}
48
+ onClick={() => {
49
+ debug()?.downloadInputAudio();
50
+ debug()?.downloadOutputAudio();
51
+ }}>
52
+ Download Input / Ouput Audio
53
+ </Button>
54
+ </>
55
+ ) : (
56
+ <Typography>No input / output detected</Typography>
57
+ )}
58
+ </AccordionDetails>
59
+ </Accordion>
60
+ </div>
61
+ );
62
+ }
streaming-react-app/src/RoomConfig.tsx ADDED
@@ -0,0 +1,263 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import Stack from '@mui/material/Stack';
2
+ import TextField from '@mui/material/TextField';
3
+ import {isValidRoomID, isValidPartialRoomID} from './generateNewRoomID';
4
+ import {useCallback, useEffect, useState} from 'react';
5
+ import Button from '@mui/material/Button';
6
+ import {useSocket} from './useSocket';
7
+ import FormGroup from '@mui/material/FormGroup';
8
+ import FormControlLabel from '@mui/material/FormControlLabel';
9
+ import Checkbox from '@mui/material/Checkbox';
10
+ import {RoomState} from './types/RoomState';
11
+ import setURLParam from './setURLParam';
12
+ import {getURLParams} from './URLParams';
13
+ import {
14
+ JoinRoomConfig,
15
+ Roles,
16
+ ServerState,
17
+ StreamingStatus,
18
+ } from './types/StreamingTypes';
19
+ import Alert from '@mui/material/Alert';
20
+
21
+ function capitalize(str: string): string {
22
+ return str.charAt(0).toUpperCase() + str.slice(1);
23
+ }
24
+
25
+ type Props = {
26
+ roomState: RoomState | null;
27
+ serverState: ServerState | null;
28
+ onJoinRoomOrUpdateRoles?: () => void;
29
+ streamingStatus: StreamingStatus;
30
+ };
31
+
32
+ export default function RoomConfig({
33
+ roomState,
34
+ serverState,
35
+ onJoinRoomOrUpdateRoles,
36
+ streamingStatus,
37
+ }: Props) {
38
+ const {socket, clientID} = useSocket();
39
+
40
+ const urlParams = getURLParams();
41
+ const roomIDParam = urlParams.roomID;
42
+ const autoJoinRoom = urlParams.autoJoin;
43
+
44
+ const [roomID, setRoomID] = useState<string>(
45
+ (roomIDParam ?? '').toUpperCase(),
46
+ );
47
+ const [roomIDError, setRoomIDError] = useState<boolean>(false);
48
+ const [roles, setRoles] = useState<{speaker: boolean; listener: boolean}>({
49
+ speaker: true,
50
+ listener: true,
51
+ });
52
+ const [lockServer, setLockServer] = useState<boolean>(false);
53
+ const [lockServerName, setLockServerName] = useState<string>('');
54
+
55
+ const [joinInProgress, setJoinInProgress] = useState<boolean>(false);
56
+ const [didAttemptAutoJoin, setDidAttemptAutoJoin] = useState<boolean>(false);
57
+
58
+ const isValidServerLock =
59
+ lockServer === false ||
60
+ (lockServerName != null && lockServerName.length > 0);
61
+ const isValidRoles = Object.values(roles).filter(Boolean).length > 0;
62
+ const isValidAllInputs =
63
+ isValidRoomID(roomID) && isValidRoles && isValidServerLock;
64
+ const roomIDFromServer = roomState?.room_id ?? null;
65
+
66
+ const onJoinRoom = useCallback(
67
+ (createNewRoom: boolean) => {
68
+ if (socket == null) {
69
+ console.error('Socket is null, cannot join room');
70
+ return;
71
+ }
72
+ console.debug(`Attempting to join roomID ${roomID}...`);
73
+
74
+ const lockServerValidated: string | null =
75
+ lockServer && roles['speaker'] ? lockServerName : null;
76
+
77
+ // TODO: Show error state if roomID isn't valid
78
+ setJoinInProgress(true);
79
+
80
+ const configObject: JoinRoomConfig = {
81
+ roles: (Object.keys(roles) as Array<Roles>).filter(
82
+ (role) => roles[role] === true,
83
+ ),
84
+ lockServerName: lockServerValidated,
85
+ };
86
+
87
+ socket.emit(
88
+ 'join_room',
89
+ clientID,
90
+ createNewRoom ? null : roomID,
91
+ configObject,
92
+ (result) => {
93
+ console.log('join_room result:', result);
94
+ if (createNewRoom) {
95
+ setRoomID(result.roomID);
96
+ }
97
+ if (onJoinRoomOrUpdateRoles != null) {
98
+ onJoinRoomOrUpdateRoles();
99
+ }
100
+ setURLParam('roomID', result.roomID);
101
+ setJoinInProgress(false);
102
+ },
103
+ );
104
+ },
105
+ [
106
+ clientID,
107
+ lockServer,
108
+ lockServerName,
109
+ onJoinRoomOrUpdateRoles,
110
+ roles,
111
+ roomID,
112
+ socket,
113
+ ],
114
+ );
115
+
116
+ useEffect(() => {
117
+ if (
118
+ autoJoinRoom === true &&
119
+ didAttemptAutoJoin === false &&
120
+ socket != null
121
+ ) {
122
+ // We want to consider this an attempt whether or not we actually try to join, because
123
+ // we only want auto-join to happen on initial load
124
+ setDidAttemptAutoJoin(true);
125
+ if (
126
+ isValidAllInputs &&
127
+ joinInProgress === false &&
128
+ roomIDFromServer == null
129
+ ) {
130
+ console.debug('Attempting to auto-join room...');
131
+
132
+ onJoinRoom(false);
133
+ } else {
134
+ console.debug('Unable to auto-join room', {
135
+ isValidAllInputs,
136
+ joinInProgress,
137
+ roomIDFromServer,
138
+ });
139
+ }
140
+ }
141
+ }, [
142
+ autoJoinRoom,
143
+ didAttemptAutoJoin,
144
+ isValidAllInputs,
145
+ joinInProgress,
146
+ onJoinRoom,
147
+ roomIDFromServer,
148
+ socket,
149
+ ]);
150
+
151
+ return (
152
+ <Stack direction="column" spacing="12px">
153
+ <Stack direction="row" spacing="12px" sx={{alignItems: 'center'}}>
154
+ <TextField
155
+ size="small"
156
+ label="Room Code"
157
+ variant="outlined"
158
+ disabled={roomState?.room_id != null}
159
+ value={roomID}
160
+ error={roomIDError}
161
+ onChange={(e) => {
162
+ const id = e.target.value.toUpperCase();
163
+ if (isValidPartialRoomID(id)) {
164
+ setRoomIDError(false);
165
+ setRoomID(id);
166
+ } else {
167
+ setRoomIDError(true);
168
+ }
169
+ }}
170
+ sx={{width: '8em'}}
171
+ />
172
+
173
+ <div>
174
+ <Button
175
+ variant="contained"
176
+ disabled={
177
+ isValidAllInputs === false ||
178
+ joinInProgress ||
179
+ streamingStatus !== 'stopped'
180
+ }
181
+ onClick={() => onJoinRoom(false)}>
182
+ {roomState?.room_id != null ? 'Update Roles' : 'Join Room'}
183
+ </Button>
184
+ </div>
185
+
186
+ {roomState?.room_id == null && (
187
+ <div>
188
+ <Button
189
+ variant="contained"
190
+ disabled={
191
+ roomState?.room_id != null ||
192
+ joinInProgress ||
193
+ streamingStatus !== 'stopped'
194
+ }
195
+ onClick={() => onJoinRoom(true)}>
196
+ {'Create New Room'}
197
+ </Button>
198
+ </div>
199
+ )}
200
+ </Stack>
201
+
202
+ <FormGroup>
203
+ {Object.keys(roles).map((role) => {
204
+ return (
205
+ <FormControlLabel
206
+ disabled={streamingStatus !== 'stopped'}
207
+ key={role}
208
+ control={
209
+ <Checkbox
210
+ checked={roles[role]}
211
+ onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
212
+ setRoles((prevRoles) => ({
213
+ ...prevRoles,
214
+ [role]: event.target.checked,
215
+ }));
216
+ }}
217
+ />
218
+ }
219
+ label={capitalize(role)}
220
+ />
221
+ );
222
+ })}
223
+
224
+ {urlParams.enableServerLock && roles['speaker'] === true && (
225
+ <>
226
+ <FormControlLabel
227
+ disabled={streamingStatus !== 'stopped'}
228
+ control={
229
+ <Checkbox
230
+ checked={lockServer}
231
+ onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
232
+ setLockServer(event.target.checked);
233
+ }}
234
+ />
235
+ }
236
+ label="Lock Server (prevent other users from streaming)"
237
+ />
238
+ </>
239
+ )}
240
+ </FormGroup>
241
+
242
+ {urlParams.enableServerLock &&
243
+ roles['speaker'] === true &&
244
+ lockServer && (
245
+ <TextField
246
+ disabled={streamingStatus !== 'stopped'}
247
+ label="Enter Your Name + Expected Lock End Time"
248
+ variant="outlined"
249
+ value={lockServerName}
250
+ onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
251
+ setLockServerName(event.target.value);
252
+ }}
253
+ helperText="Locking the server will prevent anyone else from using it until you close the page, in order to maximize server performance. Please only use this for live demos."
254
+ />
255
+ )}
256
+
257
+ {serverState?.serverLock != null &&
258
+ serverState.serverLock.clientID === clientID && (
259
+ <Alert severity="success">{`The server is now locked for your use (${serverState?.serverLock?.name}). Close this window to release the lock so that others may use the server.`}</Alert>
260
+ )}
261
+ </Stack>
262
+ );
263
+ }
streaming-react-app/src/SeamlessLogo.tsx ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ function SeamlessLogo() {
2
+ return (
3
+ <svg
4
+ width="24"
5
+ height="24"
6
+ viewBox="0 0 24 24"
7
+ fill="none"
8
+ xmlns="http://www.w3.org/2000/svg">
9
+ <circle cx="12" cy="12" r="12" fill="#1C2B33" />
10
+ <rect x="7" y="9" width="2" height="6" rx="1" fill="white" />
11
+ <rect
12
+ x="15"
13
+ y="9"
14
+ width="2"
15
+ height="6"
16
+ rx="1"
17
+ fill="white"
18
+ fill-opacity="0.5"
19
+ />
20
+ <rect
21
+ x="11"
22
+ y="6"
23
+ width="2"
24
+ height="12"
25
+ rx="1"
26
+ fill="white"
27
+ fill-opacity="0.5"
28
+ />
29
+ </svg>
30
+ );
31
+ }
32
+
33
+ export default SeamlessLogo;
streaming-react-app/src/SocketWrapper.tsx ADDED
@@ -0,0 +1,217 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import {useContext, useEffect, useMemo, useRef, useState} from 'react';
2
+ import socketIOClient, {Socket} from 'socket.io-client';
3
+ import useStable from './useStable';
4
+ import {v4 as uuidv4} from 'uuid';
5
+ import {SocketContext} from './useSocket';
6
+ import {AppResetKeyContext} from './App';
7
+ import Backdrop from '@mui/material/Backdrop';
8
+ import CircularProgress from '@mui/material/CircularProgress';
9
+ import Typography from '@mui/material/Typography';
10
+ import {getURLParams} from './URLParams';
11
+
12
+ // The time to wait before showing a "disconnected" screen upon initial app load
13
+ const INITIAL_DISCONNECT_SCREEN_DELAY = 2000;
14
+ const SERVER_URL_DEFAULT = "localhost:8000"
15
+
16
+ export default function SocketWrapper({children}) {
17
+ const [socket, setSocket] = useState<Socket | null>(null);
18
+ const [connected, setConnected] = useState<boolean | null>(null);
19
+ // Default to true:
20
+ const [willAttemptReconnect, setWillAttemptReconnect] =
21
+ useState<boolean>(true);
22
+ const serverIDRef = useRef<string | null>(null);
23
+
24
+ const setAppResetKey = useContext(AppResetKeyContext);
25
+
26
+ /**
27
+ * Previously we had stored the clientID in local storage, but in that case
28
+ * if a user refreshes their page they'll still have the same clientID, and
29
+ * will be put back into the same room, which may be confusing if they're trying
30
+ * to join a new room or reset the app interface. So now clientIDs persist only as
31
+ * long as the react app full lifecycle
32
+ */
33
+ const clientID = useStable<string>(() => {
34
+ const newID = uuidv4();
35
+ // Set the clientID in session storage so if the page reloads the person
36
+ // still retains their member/room config
37
+ return newID;
38
+ });
39
+
40
+ const socketObject = useMemo(
41
+ () => ({socket, clientID, connected: connected ?? false}),
42
+ [socket, clientID, connected],
43
+ );
44
+
45
+ useEffect(() => {
46
+ const queryParams = {
47
+ clientID: clientID,
48
+ };
49
+
50
+ const serverURLFromParams = getURLParams().serverURL;
51
+ const serverURL = serverURLFromParams ?? SERVER_URL_DEFAULT;
52
+
53
+ console.log(
54
+ `Opening socket connection to ${
55
+ serverURL?.length === 0 ? 'window.location.host' : serverURL
56
+ } with query params:`,
57
+ queryParams,
58
+ );
59
+
60
+ const newSocket: Socket = socketIOClient(serverURL, {
61
+ query: queryParams,
62
+ // Normally socket.io will fallback to http polling, but we basically never
63
+ // want that because that'd mean awful performance. It'd be better for the app
64
+ // to simply break in that case and not connect.
65
+ transports: ['websocket'],
66
+ });
67
+
68
+ const onServerID = (serverID: string) => {
69
+ console.debug('Received server ID:', serverID);
70
+ if (serverIDRef.current != null) {
71
+ if (serverIDRef.current !== serverID) {
72
+ console.error(
73
+ 'Server ID changed. Resetting the app using the app key',
74
+ );
75
+ setAppResetKey(serverID);
76
+ }
77
+ }
78
+ serverIDRef.current = serverID;
79
+ };
80
+
81
+ newSocket.on('server_id', onServerID);
82
+
83
+ setSocket(newSocket);
84
+
85
+ return () => {
86
+ newSocket.off('server_id', onServerID);
87
+ console.log(
88
+ 'Closing socket connection in the useEffect cleanup function...',
89
+ );
90
+ newSocket.disconnect();
91
+ setSocket(null);
92
+ };
93
+ }, [clientID, setAppResetKey]);
94
+
95
+ useEffect(() => {
96
+ if (socket != null) {
97
+ const onAny = (eventName: string, ...args) => {
98
+ console.debug(`[event: ${eventName}] args:`, ...args);
99
+ };
100
+
101
+ socket.onAny(onAny);
102
+
103
+ return () => {
104
+ socket.offAny(onAny);
105
+ };
106
+ }
107
+ return () => {};
108
+ }, [socket]);
109
+
110
+ useEffect(() => {
111
+ if (socket != null) {
112
+ const onConnect = (...args) => {
113
+ console.debug('Connected to server with args:', ...args);
114
+ setConnected(true);
115
+ };
116
+
117
+ const onConnectError = (err) => {
118
+ console.error(`Connection error due to ${err.message}`);
119
+ };
120
+
121
+ const onDisconnect = (reason) => {
122
+ setConnected(false);
123
+ console.log(`Disconnected due to ${reason}`);
124
+ };
125
+
126
+ socket.on('connect', onConnect);
127
+ socket.on('connect_error', onConnectError);
128
+ socket.on('disconnect', onDisconnect);
129
+
130
+ return () => {
131
+ socket.off('connect', onConnect);
132
+ socket.off('connect_error', onConnectError);
133
+ socket.off('disconnect', onDisconnect);
134
+ };
135
+ }
136
+ }, [socket]);
137
+
138
+ useEffect(() => {
139
+ if (socket != null) {
140
+ const onReconnectError = (err) => {
141
+ console.log(`Reconnect error due to ${err.message}`);
142
+ };
143
+
144
+ socket.io.on('reconnect_error', onReconnectError);
145
+
146
+ const onError = (err) => {
147
+ console.log(`General socket error with message ${err.message}`);
148
+ };
149
+ socket.io.on('error', onError);
150
+
151
+ const onReconnect = (attempt) => {
152
+ console.log(`Reconnected after ${attempt} attempt(s)`);
153
+ };
154
+ socket.io.on('reconnect', onReconnect);
155
+
156
+ const disconnectOnBeforeUnload = () => {
157
+ console.log('Disconnecting due to beforeunload event...');
158
+ socket.disconnect();
159
+ setSocket(null);
160
+ };
161
+ window.addEventListener('beforeunload', disconnectOnBeforeUnload);
162
+
163
+ return () => {
164
+ socket.io.off('reconnect_error', onReconnectError);
165
+ socket.io.off('error', onError);
166
+ socket.io.off('reconnect', onReconnect);
167
+ window.removeEventListener('beforeunload', disconnectOnBeforeUnload);
168
+ };
169
+ }
170
+ }, [clientID, setAppResetKey, socket]);
171
+
172
+ /**
173
+ * Wait to show the disconnected screen on initial app load
174
+ */
175
+ useEffect(() => {
176
+ window.setTimeout(() => {
177
+ setConnected((prev) => {
178
+ if (prev === null) {
179
+ return false;
180
+ }
181
+ return prev;
182
+ });
183
+ }, INITIAL_DISCONNECT_SCREEN_DELAY);
184
+ }, []);
185
+
186
+ return (
187
+ <SocketContext.Provider value={socketObject}>
188
+ {children}
189
+
190
+ <Backdrop
191
+ open={connected === false && willAttemptReconnect === true}
192
+ sx={{
193
+ color: '#fff',
194
+ zIndex: (theme) => theme.zIndex.drawer + 1,
195
+ }}>
196
+ <div
197
+ style={{
198
+ alignItems: 'center',
199
+ flexDirection: 'column',
200
+ textAlign: 'center',
201
+ }}>
202
+ <CircularProgress color="inherit" />
203
+ <Typography
204
+ align="center"
205
+ fontSize={{sm: 18, xs: 16}}
206
+ sx={{
207
+ fontFamily:
208
+ 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace',
209
+ fontWeight: 'bold',
210
+ }}>
211
+ {'Disconnected. Attempting to reconnect...'}
212
+ </Typography>
213
+ </div>
214
+ </Backdrop>
215
+ </SocketContext.Provider>
216
+ );
217
+ }
streaming-react-app/src/StreamingInterface.css ADDED
@@ -0,0 +1,66 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .app-wrapper-sra {
2
+ display: flex;
3
+ flex-direction: column;
4
+ justify-content: center;
5
+ align-items: center;
6
+ }
7
+
8
+ .main-container-sra {
9
+ background-color: white;
10
+ display: flex;
11
+ flex-direction: column;
12
+ justify-content: flex-start;
13
+ text-align: left;
14
+ margin: 16px;
15
+ margin-bottom: 36px;
16
+ border-radius: 8px;
17
+ box-shadow: 0px 24px 30px rgba(0, 0, 0, 0.3);
18
+ border: 1px solid rgba(0, 0, 0, 0.05);
19
+ min-height: 300px;
20
+ /* max-height: 95vh; */
21
+ /* max-width: 625px; */
22
+ /* min-width: 580px; */
23
+ overflow: hidden;
24
+ }
25
+
26
+ .top-section-sra {
27
+ padding-top: 24px;
28
+ margin-bottom: 24px;
29
+ display: flex;
30
+ flex-direction: column;
31
+ justify-content: flex-start;
32
+ }
33
+
34
+ .horizontal-padding-sra {
35
+ padding-left: 20px;
36
+ padding-right: 20px;
37
+ }
38
+
39
+ .header-container-sra {
40
+ display: flex;
41
+ flex-direction: row;
42
+ justify-content: flex-start;
43
+ align-items: center;
44
+ margin-bottom: 24px;
45
+ }
46
+
47
+ .header-icon-sra {
48
+ display: block;
49
+ margin-right: 12px;
50
+ }
51
+
52
+ .translation-text-container-sra {
53
+ background-color: #f8f8f8;
54
+ /* flex-grow: 1; make it expand to fill the available space */
55
+ padding-top: 12px;
56
+ padding-bottom: 4px;
57
+ }
58
+
59
+ .translation-text-sra {
60
+ /* overflow-y: scroll; */
61
+ /* max-height: 500px; */
62
+ }
63
+
64
+ .text-chunk-sra {
65
+ margin-bottom: 12px;
66
+ }
streaming-react-app/src/StreamingInterface.tsx ADDED
@@ -0,0 +1,1149 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import {useCallback, useEffect, useLayoutEffect, useRef, useState} from 'react';
2
+ import Button from '@mui/material/Button';
3
+ import Typography from '@mui/material/Typography';
4
+ import InputLabel from '@mui/material/InputLabel';
5
+ import FormControl from '@mui/material/FormControl';
6
+ import Select, {SelectChangeEvent} from '@mui/material/Select';
7
+ import MenuItem from '@mui/material/MenuItem';
8
+ import Stack from '@mui/material/Stack';
9
+ import seamlessLogoUrl from './assets/seamless.svg';
10
+ import {
11
+ AgentCapabilities,
12
+ BaseResponse,
13
+ BrowserAudioStreamConfig,
14
+ DynamicConfig,
15
+ PartialDynamicConfig,
16
+ SUPPORTED_INPUT_SOURCES,
17
+ SUPPORTED_OUTPUT_MODES,
18
+ ServerExceptionData,
19
+ ServerSpeechData,
20
+ ServerState,
21
+ ServerTextData,
22
+ StartStreamEventConfig,
23
+ StreamingStatus,
24
+ SupportedInputSource,
25
+ SupportedOutputMode,
26
+ TranslationSentences,
27
+ } from './types/StreamingTypes';
28
+ import FormLabel from '@mui/material/FormLabel';
29
+ import RadioGroup from '@mui/material/RadioGroup';
30
+ import FormControlLabel from '@mui/material/FormControlLabel';
31
+ import Radio from '@mui/material/Radio';
32
+ import './StreamingInterface.css';
33
+ import RoomConfig from './RoomConfig';
34
+ import Divider from '@mui/material/Divider';
35
+ import {useSocket} from './useSocket';
36
+ import {RoomState} from './types/RoomState';
37
+ import useStable from './useStable';
38
+ import float32To16BitPCM from './float32To16BitPCM';
39
+ import createBufferedSpeechPlayer from './createBufferedSpeechPlayer';
40
+ import Checkbox from '@mui/material/Checkbox';
41
+ import Alert from '@mui/material/Alert';
42
+ import ISO6391 from 'iso-639-1';
43
+ import isScrolledToDocumentBottom from './isScrolledToDocumentBottom';
44
+ import Box from '@mui/material/Box';
45
+ import Slider from '@mui/material/Slider';
46
+ import VolumeDown from '@mui/icons-material/VolumeDown';
47
+ import VolumeUp from '@mui/icons-material/VolumeUp';
48
+ import Mic from '@mui/icons-material/Mic';
49
+ import MicOff from '@mui/icons-material/MicOff';
50
+ import XRDialog from './react-xr/XRDialog';
51
+ import getTranslationSentencesFromReceivedData from './getTranslationSentencesFromReceivedData';
52
+ import {
53
+ sliceTranslationSentencesUpToIndex,
54
+ getTotalSentencesLength,
55
+ } from './sliceTranslationSentencesUtils';
56
+ import Blink from './Blink';
57
+ import {CURSOR_BLINK_INTERVAL_MS} from './cursorBlinkInterval';
58
+ import {getURLParams} from './URLParams';
59
+ import debug from './debug';
60
+ import DebugSection from './DebugSection';
61
+ import Switch from '@mui/material/Switch';
62
+
63
+ const AUDIO_STREAM_DEFAULTS: {
64
+ [key in SupportedInputSource]: BrowserAudioStreamConfig;
65
+ } = {
66
+ userMedia: {
67
+ noiseSuppression: true,
68
+ echoCancellation: false,
69
+ },
70
+ displayMedia: {
71
+ noiseSuppression: false,
72
+ echoCancellation: false,
73
+ },
74
+ };
75
+
76
+ async function requestUserMediaAudioStream(
77
+ config: BrowserAudioStreamConfig = {
78
+ noiseSuppression: true,
79
+ echoCancellation: false,
80
+ },
81
+ ) {
82
+ const stream = await navigator.mediaDevices.getUserMedia({
83
+ audio: {...config, channelCount: 1},
84
+ });
85
+ console.debug(
86
+ '[requestUserMediaAudioStream] stream created with settings:',
87
+ stream.getAudioTracks()?.[0]?.getSettings(),
88
+ );
89
+ return stream;
90
+ }
91
+
92
+ async function requestDisplayMediaAudioStream(
93
+ config: BrowserAudioStreamConfig = {
94
+ noiseSuppression: false,
95
+ echoCancellation: false,
96
+ },
97
+ ) {
98
+ const stream = await navigator.mediaDevices.getDisplayMedia({
99
+ audio: {...config, channelCount: 1},
100
+ // selfBrowserSurface: false, // don't allow the user to select the current tab as the source
101
+ });
102
+ console.debug(
103
+ '[requestDisplayMediaAudioStream] stream created with settings:',
104
+ stream.getAudioTracks()?.[0]?.getSettings(),
105
+ );
106
+ return stream;
107
+ }
108
+
109
+ const buttonLabelMap: {[key in StreamingStatus]: string} = {
110
+ stopped: 'Start Streaming',
111
+ running: 'Stop Streaming',
112
+ starting: 'Starting...',
113
+ };
114
+
115
+ const BUFFER_LIMIT = 1;
116
+
117
+ const SCROLLED_TO_BOTTOM_THRESHOLD_PX = 36;
118
+
119
+ const GAIN_MULTIPLIER_OVER_1 = 3;
120
+
121
+ const getGainScaledValue = (value) =>
122
+ value > 1 ? (value - 1) * GAIN_MULTIPLIER_OVER_1 + 1 : value;
123
+
124
+ const TOTAL_ACTIVE_TRANSCODER_WARNING_THRESHOLD = 2;
125
+
126
+ const MAX_SERVER_EXCEPTIONS_TRACKED = 500;
127
+
128
+ export const TYPING_ANIMATION_DELAY_MS = 6;
129
+
130
+ export default function StreamingInterface() {
131
+ const urlParams = getURLParams();
132
+ const debugParam = urlParams.debug;
133
+ const animateTextDisplay = urlParams.animateTextDisplay;
134
+
135
+ const socketObject = useSocket();
136
+ const {socket, clientID} = socketObject;
137
+
138
+ const [serverState, setServerState] = useState<ServerState | null>(null);
139
+ const [agent, setAgent] = useState<AgentCapabilities | null>(null);
140
+ const model = agent?.name ?? null;
141
+ const agentsCapabilities: Array<AgentCapabilities> =
142
+ serverState?.agentsCapabilities ?? [];
143
+ const currentAgent: AgentCapabilities | null =
144
+ agentsCapabilities.find((agent) => agent.name === model) ?? null;
145
+
146
+ const [serverExceptions, setServerExceptions] = useState<
147
+ Array<ServerExceptionData>
148
+ >([]);
149
+ const [connectionError, setConnectionError] = useState<string | null>(null);
150
+ const [roomState, setRoomState] = useState<RoomState | null>(null);
151
+ const roomID = roomState?.room_id ?? null;
152
+ const isSpeaker =
153
+ (clientID != null && roomState?.speakers.includes(clientID)) ?? false;
154
+ const isListener =
155
+ (clientID != null && roomState?.listeners.includes(clientID)) ?? false;
156
+
157
+ const [streamingStatus, setStreamingStatus] =
158
+ useState<StreamingStatus>('stopped');
159
+
160
+ const isStreamConfiguredRef = useRef<boolean>(false);
161
+
162
+ const [outputMode, setOutputMode] = useState<SupportedOutputMode>('s2s&t');
163
+ const [inputSource, setInputSource] =
164
+ useState<SupportedInputSource>('userMedia');
165
+ const [enableNoiseSuppression, setEnableNoiseSuppression] = useState<
166
+ boolean | null
167
+ >(null);
168
+
169
+ // Dynamic Params:
170
+ const [targetLang, setTargetLang] = useState<string | null>(null);
171
+ const [enableExpressive, setEnableExpressive] = useState<boolean | null>(
172
+ null,
173
+ );
174
+
175
+ const [serverDebugFlag, setServerDebugFlag] = useState<boolean>(
176
+ debugParam ?? false,
177
+ );
178
+
179
+ const [receivedData, setReceivedData] = useState<Array<ServerTextData>>([]);
180
+ // const [translationSentencesAnimated, setTranslationSentencesAnimated] =
181
+ // useState<TranslationSentences>([]);
182
+ const [
183
+ translationSentencesAnimatedIndex,
184
+ setTranslationSentencesAnimatedIndex,
185
+ ] = useState<number>(0);
186
+
187
+ const lastTranslationResultRef = useRef<HTMLDivElement | null>(null);
188
+
189
+ const [inputStream, setInputStream] = useState<MediaStream | null>(null);
190
+ const [inputStreamSource, setInputStreamSource] =
191
+ useState<MediaStreamAudioSourceNode | null>(null);
192
+ const audioContext = useStable<AudioContext>(() => new AudioContext());
193
+ const [scriptNodeProcessor, setScriptNodeProcessor] =
194
+ useState<ScriptProcessorNode | null>(null);
195
+
196
+ const [muted, setMuted] = useState<boolean>(false);
197
+ // The onaudioprocess script needs an up-to-date reference to the muted state, so
198
+ // we use a ref here and keep it in sync via useEffect
199
+ const mutedRef = useRef<boolean>(muted);
200
+ useEffect(() => {
201
+ mutedRef.current = muted;
202
+ }, [muted]);
203
+
204
+ const [gain, setGain] = useState<number>(1);
205
+
206
+ const isScrolledToBottomRef = useRef<boolean>(isScrolledToDocumentBottom());
207
+
208
+ // Some config options must be set when starting streaming and cannot be chaned dynamically.
209
+ // This controls whether they are disabled or not
210
+ const streamFixedConfigOptionsDisabled =
211
+ streamingStatus !== 'stopped' || roomID == null;
212
+
213
+ const bufferedSpeechPlayer = useStable(() => {
214
+ const player = createBufferedSpeechPlayer({
215
+ onStarted: () => {
216
+ console.debug('📢 PLAYBACK STARTED 📢');
217
+ },
218
+ onEnded: () => {
219
+ console.debug('🛑 PLAYBACK ENDED 🛑');
220
+ },
221
+ });
222
+
223
+ // Start the player now so it eagerly plays audio when it arrives
224
+ player.start();
225
+ return player;
226
+ });
227
+
228
+ const translationSentencesBase: TranslationSentences =
229
+ getTranslationSentencesFromReceivedData(receivedData);
230
+
231
+ const translationSentencesBaseTotalLength = getTotalSentencesLength(
232
+ translationSentencesBase,
233
+ );
234
+
235
+ const translationSentences: TranslationSentences = animateTextDisplay
236
+ ? sliceTranslationSentencesUpToIndex(
237
+ translationSentencesBase,
238
+ translationSentencesAnimatedIndex,
239
+ )
240
+ : translationSentencesBase;
241
+
242
+ // We want the blinking cursor to show before any text has arrived, so let's add an empty string so that the cursor shows up
243
+ const translationSentencesWithEmptyStartingString =
244
+ streamingStatus === 'running' && translationSentences.length === 0
245
+ ? ['']
246
+ : translationSentences;
247
+
248
+ /******************************************
249
+ * Event Handlers
250
+ ******************************************/
251
+
252
+ const setAgentAndUpdateParams = useCallback(
253
+ (newAgent: AgentCapabilities | null) => {
254
+ setAgent((prevAgent) => {
255
+ if (prevAgent?.name !== newAgent?.name) {
256
+ setTargetLang(newAgent?.targetLangs[0] ?? null);
257
+ setEnableExpressive(null);
258
+ // setOutputMode(newAgent.modalities[0]);
259
+ }
260
+ return newAgent;
261
+ });
262
+ },
263
+ [],
264
+ );
265
+
266
+ const onSetDynamicConfig = useCallback(
267
+ async (partialConfig: PartialDynamicConfig) => {
268
+ return new Promise<void>((resolve, reject) => {
269
+ if (socket == null) {
270
+ reject(new Error('[onSetDynamicConfig] socket is null '));
271
+ return;
272
+ }
273
+
274
+ socket.emit(
275
+ 'set_dynamic_config',
276
+ partialConfig,
277
+ (result: BaseResponse) => {
278
+ console.log('[emit result: set_dynamic_config]', result);
279
+ if (result.status === 'ok') {
280
+ resolve();
281
+ } else {
282
+ reject();
283
+ }
284
+ },
285
+ );
286
+ });
287
+ },
288
+ [socket],
289
+ );
290
+
291
+ const configureStreamAsync = ({sampleRate}: {sampleRate: number}) => {
292
+ return new Promise<void>((resolve, reject) => {
293
+ if (socket == null) {
294
+ reject(new Error('[configureStreamAsync] socket is null '));
295
+ return;
296
+ }
297
+ const modelName = agent?.name ?? null;
298
+ if (modelName == null) {
299
+ reject(new Error('[configureStreamAsync] modelName is null '));
300
+ return;
301
+ }
302
+
303
+ const config: StartStreamEventConfig = {
304
+ event: 'config',
305
+ rate: sampleRate,
306
+ model_name: modelName,
307
+ // source_language: inputLang,
308
+ debug: serverDebugFlag,
309
+ // synchronous processing isn't implemented on the v2 pubsub server, so hardcode this to true
310
+ async_processing: true,
311
+ buffer_limit: BUFFER_LIMIT,
312
+ model_type: outputMode,
313
+ };
314
+
315
+ console.log('[configureStreamAsync] sending config', config);
316
+
317
+ socket.emit('configure_stream', config, (statusObject) => {
318
+ if (statusObject.status === 'ok') {
319
+ isStreamConfiguredRef.current = true;
320
+ console.debug(
321
+ '[configureStreamAsync] stream configured!',
322
+ statusObject,
323
+ );
324
+ resolve();
325
+ } else {
326
+ isStreamConfiguredRef.current = false;
327
+ reject(
328
+ new Error(
329
+ `[configureStreamAsync] configure_stream returned status: ${statusObject.status}`,
330
+ ),
331
+ );
332
+ return;
333
+ }
334
+ });
335
+ });
336
+ };
337
+
338
+ const startStreaming = async () => {
339
+ if (streamingStatus !== 'stopped') {
340
+ console.warn(
341
+ `Attempting to start stream when status is ${streamingStatus}`,
342
+ );
343
+ return;
344
+ }
345
+
346
+ setStreamingStatus('starting');
347
+
348
+ if (audioContext.state === 'suspended') {
349
+ console.warn('audioContext was suspended! resuming...');
350
+ await audioContext.resume();
351
+ }
352
+
353
+ let stream: MediaStream | null = null;
354
+
355
+ try {
356
+ if (inputSource === 'userMedia') {
357
+ stream = await requestUserMediaAudioStream({
358
+ noiseSuppression:
359
+ enableNoiseSuppression ??
360
+ AUDIO_STREAM_DEFAULTS['userMedia'].noiseSuppression,
361
+ echoCancellation: false,
362
+ });
363
+ } else if (inputSource === 'displayMedia') {
364
+ stream = await requestDisplayMediaAudioStream({
365
+ noiseSuppression:
366
+ enableNoiseSuppression ??
367
+ AUDIO_STREAM_DEFAULTS['displayMedia'].noiseSuppression,
368
+ echoCancellation: false,
369
+ });
370
+ } else {
371
+ throw new Error(`Unsupported input source requested: ${inputSource}`);
372
+ }
373
+ setInputStream(stream);
374
+ } catch (e) {
375
+ console.error('[startStreaming] media stream request failed:', e);
376
+ setStreamingStatus('stopped');
377
+ return;
378
+ }
379
+
380
+ const mediaStreamSource = audioContext.createMediaStreamSource(stream);
381
+ setInputStreamSource(mediaStreamSource);
382
+ /**
383
+ * NOTE: This currently uses a deprecated way of processing the audio (createScriptProcessor).
384
+ *
385
+ * Documentation for the deprecated way of doing it is here: https://developer.mozilla.org/en-US/docs/Web/API/BaseAudioContext/createScriptProcessor
386
+ *
387
+ * This should be migrated to something like this SO answer: https://stackoverflow.com/a/65448287
388
+ */
389
+ const scriptProcessor = audioContext.createScriptProcessor(16384, 1, 1);
390
+ setScriptNodeProcessor(scriptProcessor);
391
+
392
+ scriptProcessor.onaudioprocess = (event) => {
393
+ if (isStreamConfiguredRef.current === false) {
394
+ console.debug('[onaudioprocess] stream is not configured yet!');
395
+ return;
396
+ }
397
+ if (socket == null) {
398
+ console.warn('[onaudioprocess] socket is null in onaudioprocess');
399
+ return;
400
+ }
401
+ // console.debug('[onaudioprocess] event', event);
402
+
403
+ if (mutedRef.current) {
404
+ // We still want to send audio to the server when we're muted to ensure we
405
+ // get any remaining audio back from the server, so let's pass an array length 1 with a value of 0
406
+ const mostlyEmptyInt16Array = new Int16Array(1);
407
+ socket.emit('incoming_audio', mostlyEmptyInt16Array);
408
+ } else {
409
+ const float32Audio = event.inputBuffer.getChannelData(0);
410
+ const pcm16Audio = float32To16BitPCM(float32Audio);
411
+ socket.emit('incoming_audio', pcm16Audio);
412
+ }
413
+
414
+ debug()?.sentAudio(event);
415
+ };
416
+
417
+ mediaStreamSource.connect(scriptProcessor);
418
+ scriptProcessor.connect(audioContext.destination);
419
+
420
+ bufferedSpeechPlayer.start();
421
+
422
+ try {
423
+ if (targetLang == null) {
424
+ throw new Error('[startStreaming] targetLang cannot be nullish');
425
+ }
426
+
427
+ // When we are starting the stream we want to pass all the dynamic config values
428
+ // available before actually configuring and starting the stream
429
+ const fullDynamicConfig: DynamicConfig = {
430
+ targetLanguage: targetLang,
431
+ expressive: enableExpressive,
432
+ };
433
+
434
+ await onSetDynamicConfig(fullDynamicConfig);
435
+
436
+ // NOTE: this needs to be the *audioContext* sample rate, not the sample rate of the input stream. Not entirely sure why.
437
+ await configureStreamAsync({
438
+ sampleRate: audioContext.sampleRate,
439
+ });
440
+ } catch (e) {
441
+ console.error('configureStreamAsync failed', e);
442
+ setStreamingStatus('stopped');
443
+ return;
444
+ }
445
+
446
+ setStreamingStatus('running');
447
+ };
448
+
449
+ const stopStreaming = useCallback(async () => {
450
+ if (streamingStatus === 'stopped') {
451
+ console.warn(
452
+ `Attempting to stop stream when status is ${streamingStatus}`,
453
+ );
454
+ return;
455
+ }
456
+
457
+ // Stop the speech playback right away
458
+ bufferedSpeechPlayer.stop();
459
+
460
+ if (inputStreamSource == null || scriptNodeProcessor == null) {
461
+ console.error(
462
+ 'inputStreamSource || scriptNodeProcessor is null in stopStreaming',
463
+ );
464
+ } else {
465
+ inputStreamSource.disconnect(scriptNodeProcessor);
466
+ scriptNodeProcessor.disconnect(audioContext.destination);
467
+
468
+ // From: https://stackoverflow.com/questions/65447236/scriptnode-onaudioprocess-is-deprecated-any-alternative
469
+ // do we also need this??
470
+ // recorder?.stop();
471
+
472
+ // Release the mic input so we stop showing the red recording icon in the browser
473
+ inputStream?.getTracks().forEach((track) => track.stop());
474
+ }
475
+
476
+ if (socket == null) {
477
+ console.warn('Unable to emit stop_stream because socket is null');
478
+ } else {
479
+ socket.emit('stop_stream', (result) => {
480
+ console.debug('[emit result: stop_stream]', result);
481
+ });
482
+ }
483
+
484
+ setStreamingStatus('stopped');
485
+ }, [
486
+ audioContext.destination,
487
+ bufferedSpeechPlayer,
488
+ inputStream,
489
+ inputStreamSource,
490
+ scriptNodeProcessor,
491
+ socket,
492
+ streamingStatus,
493
+ ]);
494
+
495
+ const onClearTranscriptForAll = useCallback(() => {
496
+ if (socket != null) {
497
+ socket.emit('clear_transcript_for_all');
498
+ }
499
+ }, [socket]);
500
+
501
+ /******************************************
502
+ * Effects
503
+ ******************************************/
504
+
505
+ useEffect(() => {
506
+ if (socket == null) {
507
+ return;
508
+ }
509
+
510
+ const onRoomStateUpdate = (roomState: RoomState) => {
511
+ // console.log('[event: room_state_update]', roomState);
512
+ setRoomState(roomState);
513
+ };
514
+
515
+ socket.on('room_state_update', onRoomStateUpdate);
516
+
517
+ return () => {
518
+ socket.off('room_state_update', onRoomStateUpdate);
519
+ };
520
+ }, [socket]);
521
+
522
+ useEffect(() => {
523
+ if (socket != null) {
524
+ const onTranslationText = (data: ServerTextData) => {
525
+ setReceivedData((prev) => [...prev, data]);
526
+ debug()?.receivedText(data.payload);
527
+ };
528
+
529
+ const onTranslationSpeech = (data: ServerSpeechData) => {
530
+ bufferedSpeechPlayer.addAudioToBuffer(data.payload, data.sample_rate);
531
+ };
532
+
533
+ socket.on('translation_text', onTranslationText);
534
+ socket.on('translation_speech', onTranslationSpeech);
535
+
536
+ return () => {
537
+ socket.off('translation_text', onTranslationText);
538
+ socket.off('translation_speech', onTranslationSpeech);
539
+ };
540
+ }
541
+ }, [bufferedSpeechPlayer, socket]);
542
+
543
+ useEffect(() => {
544
+ if (socket != null) {
545
+ const onServerStateUpdate = (newServerState: ServerState) => {
546
+ setServerState(newServerState);
547
+
548
+ // If a client creates a server lock, we want to stop streaming if we're not them
549
+ if (
550
+ newServerState.serverLock?.isActive === true &&
551
+ newServerState.serverLock?.clientID !== clientID &&
552
+ streamingStatus === 'running'
553
+ ) {
554
+ stopStreaming();
555
+ }
556
+
557
+ const firstAgentNullable = newServerState.agentsCapabilities[0];
558
+ if (agent == null && firstAgentNullable != null) {
559
+ setAgentAndUpdateParams(firstAgentNullable);
560
+ }
561
+ };
562
+
563
+ socket.on('server_state_update', onServerStateUpdate);
564
+
565
+ return () => {
566
+ socket.off('server_state_update', onServerStateUpdate);
567
+ };
568
+ }
569
+ }, [
570
+ agent,
571
+ clientID,
572
+ setAgentAndUpdateParams,
573
+ socket,
574
+ stopStreaming,
575
+ streamingStatus,
576
+ ]);
577
+
578
+ useEffect(() => {
579
+ if (socket != null) {
580
+ const onServerException = (
581
+ exceptionDataWithoutClientTime: ServerExceptionData,
582
+ ) => {
583
+ const exceptionData = {
584
+ ...exceptionDataWithoutClientTime,
585
+ timeStringClient: new Date(
586
+ exceptionDataWithoutClientTime['timeEpochMs'],
587
+ ).toLocaleString(),
588
+ };
589
+
590
+ setServerExceptions((prev) =>
591
+ [exceptionData, ...prev].slice(0, MAX_SERVER_EXCEPTIONS_TRACKED),
592
+ );
593
+ console.error(
594
+ `[server_exception] The server encountered an exception: ${exceptionData['message']}`,
595
+ exceptionData,
596
+ );
597
+ };
598
+
599
+ socket.on('server_exception', onServerException);
600
+
601
+ return () => {
602
+ socket.off('server_exception', onServerException);
603
+ };
604
+ }
605
+ }, [socket]);
606
+
607
+ useEffect(() => {
608
+ if (socket != null) {
609
+ const onClearTranscript = () => {
610
+ setReceivedData([]);
611
+ setTranslationSentencesAnimatedIndex(0);
612
+ };
613
+
614
+ socket.on('clear_transcript', onClearTranscript);
615
+
616
+ return () => {
617
+ socket.off('clear_transcript', onClearTranscript);
618
+ };
619
+ }
620
+ }, [socket]);
621
+
622
+ useEffect(() => {
623
+ const onScroll = () => {
624
+ if (isScrolledToDocumentBottom(SCROLLED_TO_BOTTOM_THRESHOLD_PX)) {
625
+ // console.debug('scrolled to bottom!');
626
+ isScrolledToBottomRef.current = true;
627
+ return;
628
+ }
629
+ // console.debug('NOT scrolled to bottom!');
630
+ isScrolledToBottomRef.current = false;
631
+ return;
632
+ };
633
+
634
+ document.addEventListener('scroll', onScroll);
635
+
636
+ return () => {
637
+ document.removeEventListener('scroll', onScroll);
638
+ };
639
+ }, []);
640
+
641
+ useLayoutEffect(() => {
642
+ if (
643
+ lastTranslationResultRef.current != null &&
644
+ isScrolledToBottomRef.current
645
+ ) {
646
+ // Scroll the div to the most recent entry
647
+ lastTranslationResultRef.current.scrollIntoView();
648
+ }
649
+ // Run the effect every time data is received, so that
650
+ // we scroll to the bottom even if we're just adding text to
651
+ // a pre-existing chunk
652
+ }, [receivedData]);
653
+
654
+ useEffect(() => {
655
+ if (!animateTextDisplay) {
656
+ return;
657
+ }
658
+
659
+ if (
660
+ translationSentencesAnimatedIndex < translationSentencesBaseTotalLength
661
+ ) {
662
+ const timeout = setTimeout(() => {
663
+ setTranslationSentencesAnimatedIndex((prev) => prev + 1);
664
+ debug()?.startRenderText();
665
+ }, TYPING_ANIMATION_DELAY_MS);
666
+
667
+ return () => clearTimeout(timeout);
668
+ } else {
669
+ debug()?.endRenderText();
670
+ }
671
+ }, [
672
+ animateTextDisplay,
673
+ translationSentencesAnimatedIndex,
674
+ translationSentencesBaseTotalLength,
675
+ ]);
676
+
677
+ /******************************************
678
+ * Sub-components
679
+ ******************************************/
680
+
681
+ const volumeSliderNode = (
682
+ <Stack
683
+ spacing={2}
684
+ direction="row"
685
+ sx={{mb: 1, width: '100%'}}
686
+ alignItems="center">
687
+ <VolumeDown color="primary" />
688
+ <Slider
689
+ aria-label="Volume"
690
+ defaultValue={1}
691
+ scale={getGainScaledValue}
692
+ min={0}
693
+ max={3}
694
+ step={0.1}
695
+ marks={[
696
+ {value: 0, label: '0%'},
697
+ {value: 1, label: '100%'},
698
+ {value: 2, label: '400%'},
699
+ {value: 3, label: '700%'},
700
+ ]}
701
+ valueLabelFormat={(value) => `${(value * 100).toFixed(0)}%`}
702
+ valueLabelDisplay="auto"
703
+ value={gain}
704
+ onChange={(_event: Event, newValue: number | number[]) => {
705
+ // console.log({event, newValue});
706
+ if (typeof newValue === 'number') {
707
+ const scaledGain = getGainScaledValue(newValue);
708
+ // We want the actual gain node to use the scaled value
709
+ bufferedSpeechPlayer.setGain(scaledGain);
710
+ // But we want react state to keep track of the non-scaled value
711
+ setGain(newValue);
712
+ } else {
713
+ console.error(
714
+ `[volume slider] Unexpected non-number value: ${newValue}`,
715
+ );
716
+ }
717
+ }}
718
+ />
719
+ <VolumeUp color="primary" />
720
+ </Stack>
721
+ );
722
+
723
+ const xrDialogComponent = (
724
+ <XRDialog
725
+ animateTextDisplay={
726
+ animateTextDisplay &&
727
+ translationSentencesAnimatedIndex == translationSentencesBaseTotalLength
728
+ }
729
+ bufferedSpeechPlayer={bufferedSpeechPlayer}
730
+ translationSentences={translationSentences}
731
+ roomState={roomState}
732
+ roomID={roomID}
733
+ startStreaming={startStreaming}
734
+ stopStreaming={stopStreaming}
735
+ debugParam={debugParam}
736
+ />
737
+ );
738
+
739
+ return (
740
+ <div className="app-wrapper-sra">
741
+ <Box
742
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
743
+ // @ts-ignore Not sure why it's complaining about complexity here
744
+ minWidth={'550px'}
745
+ maxWidth={'660px'}>
746
+ <div className="main-container-sra">
747
+ <div className="top-section-sra horizontal-padding-sra">
748
+ <div className="header-container-sra">
749
+ <img
750
+ src={seamlessLogoUrl}
751
+ className="header-icon-sra"
752
+ alt="Seamless Translation Logo"
753
+ height={24}
754
+ width={24}
755
+ />
756
+
757
+ <div>
758
+ <Typography variant="h1" sx={{color: '#65676B'}}>
759
+ Seamless Translation
760
+ </Typography>
761
+ </div>
762
+ </div>
763
+
764
+ <Stack spacing="22px" direction="column">
765
+ <Box>
766
+ <RoomConfig
767
+ roomState={roomState}
768
+ serverState={serverState}
769
+ streamingStatus={streamingStatus}
770
+ onJoinRoomOrUpdateRoles={() => {
771
+ // If the user has switched from speaker to listener we need to tell the
772
+ // player to play eagerly, since currently the listener doesn't have any stop/start controls
773
+ bufferedSpeechPlayer.start();
774
+ }}
775
+ />
776
+
777
+ {isListener && !isSpeaker && (
778
+ <Box
779
+ sx={{
780
+ paddingX: 6,
781
+ paddingBottom: 2,
782
+ marginY: 2,
783
+ display: 'flex',
784
+ flexDirection: 'column',
785
+ alignItems: 'center',
786
+ }}>
787
+ {volumeSliderNode}
788
+ </Box>
789
+ )}
790
+ </Box>
791
+
792
+ {isSpeaker && (
793
+ <>
794
+ <Divider />
795
+
796
+ <Stack spacing="12px" direction="column">
797
+ <FormLabel id="output-modes-radio-group-label">
798
+ Model
799
+ </FormLabel>
800
+ <FormControl
801
+ disabled={
802
+ streamFixedConfigOptionsDisabled ||
803
+ agentsCapabilities.length === 0
804
+ }
805
+ fullWidth
806
+ sx={{minWidth: '14em'}}>
807
+ <InputLabel id="model-selector-input-label">
808
+ Model
809
+ </InputLabel>
810
+ <Select
811
+ labelId="model-selector-input-label"
812
+ label="Model"
813
+ onChange={(e: SelectChangeEvent) => {
814
+ const newAgent =
815
+ agentsCapabilities.find(
816
+ (agent) => e.target.value === agent.name,
817
+ ) ?? null;
818
+ if (newAgent == null) {
819
+ console.error(
820
+ 'Unable to find agent with name',
821
+ e.target.value,
822
+ );
823
+ }
824
+ setAgentAndUpdateParams(newAgent);
825
+ }}
826
+ value={model ?? ''}>
827
+ {agentsCapabilities.map((agent) => (
828
+ <MenuItem value={agent.name} key={agent.name}>
829
+ {agent.name}
830
+ </MenuItem>
831
+ ))}
832
+ </Select>
833
+ </FormControl>
834
+
835
+ <Typography variant="body2">
836
+ {`Supported Source Languages: ${
837
+ currentAgent?.sourceLangs.join(', ') ?? 'None'
838
+ }`}
839
+ </Typography>
840
+ </Stack>
841
+
842
+ <Stack spacing={0.5}>
843
+ <FormLabel id="output-modes-radio-group-label">
844
+ Output
845
+ </FormLabel>
846
+
847
+ <Box sx={{paddingTop: 2, paddingBottom: 1}}>
848
+ <FormControl fullWidth sx={{minWidth: '14em'}}>
849
+ <InputLabel id="target-selector-input-label">
850
+ Target Language
851
+ </InputLabel>
852
+ <Select
853
+ labelId="target-selector-input-label"
854
+ label="Target Language"
855
+ onChange={(e: SelectChangeEvent) => {
856
+ setTargetLang(e.target.value);
857
+ onSetDynamicConfig({
858
+ targetLanguage: e.target.value,
859
+ });
860
+ }}
861
+ value={targetLang ?? ''}>
862
+ {currentAgent?.targetLangs.map((langCode) => (
863
+ <MenuItem value={langCode} key={langCode}>
864
+ {`${ISO6391.getName(langCode)} (${langCode})`}
865
+ </MenuItem>
866
+ ))}
867
+ </Select>
868
+ </FormControl>
869
+ </Box>
870
+
871
+ <Stack direction="row" spacing={3.5}>
872
+ <FormControl disabled={streamFixedConfigOptionsDisabled}>
873
+ <RadioGroup
874
+ aria-labelledby="output-modes-radio-group-label"
875
+ value={outputMode}
876
+ onChange={(e) =>
877
+ setOutputMode(e.target.value as SupportedOutputMode)
878
+ }
879
+ name="output-modes-radio-buttons-group">
880
+ {
881
+ // TODO: Use supported modalities from agentCapabilities
882
+ SUPPORTED_OUTPUT_MODES.map(({value, label}) => (
883
+ <FormControlLabel
884
+ key={value}
885
+ value={value}
886
+ control={<Radio />}
887
+ label={label}
888
+ />
889
+ ))
890
+ }
891
+ </RadioGroup>
892
+ </FormControl>
893
+
894
+ <Stack
895
+ direction="column"
896
+ spacing={1}
897
+ alignItems="flex-start"
898
+ sx={{flexGrow: 1}}>
899
+ {currentAgent?.dynamicParams?.includes(
900
+ 'expressive',
901
+ ) && (
902
+ <FormControlLabel
903
+ control={
904
+ <Switch
905
+ checked={enableExpressive ?? false}
906
+ onChange={(
907
+ event: React.ChangeEvent<HTMLInputElement>,
908
+ ) => {
909
+ const newValue = event.target.checked;
910
+ setEnableExpressive(newValue);
911
+ onSetDynamicConfig({expressive: newValue});
912
+ }}
913
+ />
914
+ }
915
+ label="Expressive"
916
+ />
917
+ )}
918
+
919
+ {isListener && (
920
+ <Box
921
+ sx={{
922
+ flexGrow: 1,
923
+ paddingX: 1.5,
924
+ paddingY: 1.5,
925
+ width: '100%',
926
+ }}>
927
+ {volumeSliderNode}
928
+ </Box>
929
+ )}
930
+ </Stack>
931
+ </Stack>
932
+ </Stack>
933
+
934
+ <Stack
935
+ direction="row"
936
+ spacing={2}
937
+ justifyContent="space-between">
938
+ <div>
939
+ <FormControl disabled={streamFixedConfigOptionsDisabled}>
940
+ <FormLabel id="input-source-radio-group-label">
941
+ Input Source
942
+ </FormLabel>
943
+ <RadioGroup
944
+ aria-labelledby="input-source-radio-group-label"
945
+ value={inputSource}
946
+ onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
947
+ setInputSource(
948
+ e.target.value as SupportedInputSource,
949
+ )
950
+ }
951
+ name="input-source-radio-buttons-group">
952
+ {SUPPORTED_INPUT_SOURCES.map(({label, value}) => (
953
+ <FormControlLabel
954
+ key={value}
955
+ value={value}
956
+ control={<Radio />}
957
+ label={label}
958
+ />
959
+ ))}
960
+ </RadioGroup>
961
+ </FormControl>
962
+ </div>
963
+ <div>
964
+ <FormControl disabled={streamFixedConfigOptionsDisabled}>
965
+ <FormLabel>Options</FormLabel>
966
+ <FormControlLabel
967
+ control={
968
+ <Checkbox
969
+ checked={
970
+ enableNoiseSuppression ??
971
+ AUDIO_STREAM_DEFAULTS[inputSource]
972
+ .noiseSuppression
973
+ }
974
+ onChange={(
975
+ event: React.ChangeEvent<HTMLInputElement>,
976
+ ) =>
977
+ setEnableNoiseSuppression(event.target.checked)
978
+ }
979
+ />
980
+ }
981
+ label="Noise Suppression (Browser)"
982
+ />
983
+ <FormControlLabel
984
+ control={
985
+ <Checkbox
986
+ checked={serverDebugFlag}
987
+ onChange={(
988
+ event: React.ChangeEvent<HTMLInputElement>,
989
+ ) => setServerDebugFlag(event.target.checked)}
990
+ />
991
+ }
992
+ label="Server Debug Flag"
993
+ />
994
+ </FormControl>
995
+ </div>
996
+ </Stack>
997
+
998
+ <Stack direction="row" spacing={2}>
999
+ {streamingStatus === 'stopped' ? (
1000
+ <Button
1001
+ variant="contained"
1002
+ onClick={startStreaming}
1003
+ disabled={
1004
+ roomID == null ||
1005
+ // Prevent users from starting streaming if there is a server lock with an active session
1006
+ (serverState?.serverLock?.isActive === true &&
1007
+ serverState.serverLock.clientID !== clientID)
1008
+ }>
1009
+ {buttonLabelMap[streamingStatus]}
1010
+ </Button>
1011
+ ) : (
1012
+ <Button
1013
+ variant="contained"
1014
+ color={
1015
+ streamingStatus === 'running' ? 'error' : 'primary'
1016
+ }
1017
+ disabled={
1018
+ streamingStatus === 'starting' || roomID == null
1019
+ }
1020
+ onClick={stopStreaming}>
1021
+ {buttonLabelMap[streamingStatus]}
1022
+ </Button>
1023
+ )}
1024
+
1025
+ <Box>
1026
+ <Button
1027
+ variant="contained"
1028
+ aria-label={muted ? 'Unmute' : 'Mute'}
1029
+ color={muted ? 'info' : 'primary'}
1030
+ onClick={() => setMuted((prev) => !prev)}
1031
+ sx={{
1032
+ borderRadius: 100,
1033
+ paddingX: 0,
1034
+ minWidth: '36px',
1035
+ }}>
1036
+ {muted ? <MicOff /> : <Mic />}
1037
+ </Button>
1038
+ </Box>
1039
+
1040
+ {roomID == null ? null : (
1041
+ <Box
1042
+ sx={{
1043
+ flexGrow: 1,
1044
+ display: 'flex',
1045
+ justifyContent: 'flex-end',
1046
+ }}>
1047
+ {xrDialogComponent}
1048
+ </Box>
1049
+ )}
1050
+ </Stack>
1051
+
1052
+ {serverExceptions.length > 0 && (
1053
+ <div>
1054
+ <Alert severity="error">
1055
+ {`The server encountered an exception. See the browser console for details. You may need to refresh the page to continue using the app.`}
1056
+ </Alert>
1057
+ </div>
1058
+ )}
1059
+
1060
+ {serverState != null &&
1061
+ serverState.totalActiveTranscoders >=
1062
+ TOTAL_ACTIVE_TRANSCODER_WARNING_THRESHOLD && (
1063
+ <div>
1064
+ <Alert severity="warning">
1065
+ {`The server currently has ${serverState?.totalActiveTranscoders} active streaming sessions. Performance may be degraded.`}
1066
+ </Alert>
1067
+ </div>
1068
+ )}
1069
+
1070
+ {serverState?.serverLock != null &&
1071
+ serverState.serverLock.clientID !== clientID && (
1072
+ <div>
1073
+ <Alert severity="warning">
1074
+ {`The server is currently locked by "${serverState.serverLock.name}". Priority will be given to that client when they are streaming, and your streaming session may be halted abruptly.`}
1075
+ </Alert>
1076
+ </div>
1077
+ )}
1078
+ </>
1079
+ )}
1080
+ </Stack>
1081
+
1082
+ {isListener && !isSpeaker && (
1083
+ <Box sx={{marginBottom: 1, marginTop: 2}}>
1084
+ {xrDialogComponent}
1085
+ </Box>
1086
+ )}
1087
+ </div>
1088
+
1089
+ {debugParam && roomID != null && <DebugSection />}
1090
+
1091
+ <div className="translation-text-container-sra horizontal-padding-sra">
1092
+ <Stack
1093
+ direction="row"
1094
+ spacing={2}
1095
+ sx={{mb: '16px', alignItems: 'center'}}>
1096
+ <Typography variant="h1" sx={{fontWeight: 700, flexGrow: 1}}>
1097
+ Transcript
1098
+ </Typography>
1099
+ {isSpeaker && (
1100
+ <Button
1101
+ variant="text"
1102
+ size="small"
1103
+ onClick={onClearTranscriptForAll}>
1104
+ Clear Transcript for All
1105
+ </Button>
1106
+ )}
1107
+ </Stack>
1108
+ <Stack direction="row">
1109
+ <div className="translation-text-sra">
1110
+ {translationSentencesWithEmptyStartingString.map(
1111
+ (sentence, index, arr) => {
1112
+ const isLast = index === arr.length - 1;
1113
+ const maybeRef = isLast
1114
+ ? {ref: lastTranslationResultRef}
1115
+ : {};
1116
+ return (
1117
+ <div className="text-chunk-sra" key={index} {...maybeRef}>
1118
+ <Typography variant="body1">
1119
+ {sentence}
1120
+ {animateTextDisplay && isLast && (
1121
+ <Blink
1122
+ intervalMs={CURSOR_BLINK_INTERVAL_MS}
1123
+ shouldBlink={
1124
+ (roomState?.activeTranscoders ?? 0) > 0
1125
+ }>
1126
+ <Typography
1127
+ component="span"
1128
+ variant="body1"
1129
+ sx={{
1130
+ display: 'inline-block',
1131
+ transform: 'scaleY(1.25) translateY(-1px)',
1132
+ }}>
1133
+ {'|'}
1134
+ </Typography>
1135
+ </Blink>
1136
+ )}
1137
+ </Typography>
1138
+ </div>
1139
+ );
1140
+ },
1141
+ )}
1142
+ </div>
1143
+ </Stack>
1144
+ </div>
1145
+ </div>
1146
+ </Box>
1147
+ </div>
1148
+ );
1149
+ }
streaming-react-app/src/URLParams.ts ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import {getBooleanParamFlag, getStringParamFlag} from './getParamFlag';
2
+ import {URLParamsObject} from './types/URLParamsTypes';
3
+
4
+ /**
5
+ * These are the URL parameters you can provide to the app to change its behavior.
6
+ *
7
+ * Boolean flags can be set by just providing the flag name (`?autoJoin`), or by
8
+ * explicitly setting it to 1 (true) or 0 (false): `?autoJoin=1` or `?autoJoin=0`
9
+ *
10
+ * String flags require an explicit value: `?roomID=ABCD`
11
+ *
12
+ * Examples:
13
+ *
14
+ * - `http://localhost:5173/?roomID=BBCD&autoJoin&debug`
15
+ * - `http://localhost:5173/?serverURL=localhost:8000`
16
+
17
+ * @returns
18
+ */
19
+
20
+ export function getURLParams(): URLParamsObject {
21
+ return {
22
+ // animate the translation text when it arrives, typing it out one letter at a time
23
+ animateTextDisplay: getBooleanParamFlag('animateTextDisplay', true), // default to true;
24
+
25
+ // automatically join the room when the app loads. requires roomID to be set via url param as well
26
+ autoJoin: getBooleanParamFlag('autoJoin', false),
27
+
28
+ // automatically check the server debug flag as true
29
+ debug: getBooleanParamFlag('debug', false),
30
+
31
+ // Enable UI on the client that allows locking out other users of the server when it's being used for high profile demos
32
+ // NOTE: There is an escape hatch for disabling a server lock by setting the name field to remove_server_lock
33
+ enableServerLock: getBooleanParamFlag('enableServerLock', false),
34
+
35
+ // Pre-populate the Room Code field with the provided roomID. Can be used in conjunction with autoJoin to jump straight into the room
36
+ roomID: getStringParamFlag('roomID'),
37
+
38
+ // Use an alternate server URL as the streaming server (useful for pointing to dev servers: http://localhost:5173/?serverURL=localhost:8000)
39
+ serverURL: getStringParamFlag('serverURL'),
40
+
41
+ // Skip the popup dialog that displays within VR, which is mostly redundant with the web based dialog
42
+ skipARIntro: getBooleanParamFlag('skipARIntro', true), // default to true
43
+
44
+ // Shows the translation text in AR in front of an opaque panel covering all the text area
45
+ // single_block = original single text block with background
46
+ // lines = each line is a separate block and animates
47
+ // lines_with_background = adds a panel behind lines
48
+ ARTranscriptionType: getStringParamFlag('ARTranscriptionType') || 'lines',
49
+ };
50
+ }
streaming-react-app/src/assets/Roboto-msdf.json ADDED
The diff for this file is too large to render. See raw diff
 
streaming-react-app/src/assets/Roboto-msdf.png ADDED
streaming-react-app/src/assets/RobotoMono-Regular-msdf.json ADDED
The diff for this file is too large to render. See raw diff
 
streaming-react-app/src/assets/RobotoMono-Regular.png ADDED
streaming-react-app/src/assets/seamless.svg ADDED
streaming-react-app/src/createBufferedSpeechPlayer.ts ADDED
@@ -0,0 +1,177 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import debug from './debug';
2
+
3
+ type AddAudioToBufferFunction = (
4
+ samples: Array<number>,
5
+ sampleRate: number,
6
+ ) => void;
7
+
8
+ export type BufferedSpeechPlayer = {
9
+ addAudioToBuffer: AddAudioToBufferFunction;
10
+ setGain: (gain: number) => void;
11
+ start: () => void;
12
+ stop: () => void;
13
+ };
14
+
15
+ type Options = {
16
+ onEnded?: () => void;
17
+ onStarted?: () => void;
18
+ };
19
+
20
+ export default function createBufferedSpeechPlayer({
21
+ onStarted,
22
+ onEnded,
23
+ }: Options): BufferedSpeechPlayer {
24
+ const audioContext = new AudioContext();
25
+ const gainNode = audioContext.createGain();
26
+ gainNode.connect(audioContext.destination);
27
+
28
+ let unplayedAudioBuffers: Array<AudioBuffer> = [];
29
+
30
+ let currentPlayingBufferSource: AudioBufferSourceNode | null = null;
31
+
32
+ let isPlaying = false;
33
+
34
+ // This means that the player starts in the 'stopped' state, and you need to call player.start() for it to start playing
35
+ let shouldPlayWhenAudioAvailable = false;
36
+
37
+ const setGain = (gain: number) => {
38
+ gainNode.gain.setValueAtTime(gain, audioContext.currentTime);
39
+ };
40
+
41
+ const start = () => {
42
+ shouldPlayWhenAudioAvailable = true;
43
+ debug()?.start();
44
+ playNextBufferIfNotAlreadyPlaying();
45
+ };
46
+
47
+ // Stop will stop the audio and clear the buffers
48
+ const stop = () => {
49
+ shouldPlayWhenAudioAvailable = false;
50
+
51
+ // Stop the current buffers
52
+ currentPlayingBufferSource?.stop();
53
+ currentPlayingBufferSource = null;
54
+
55
+ unplayedAudioBuffers = [];
56
+
57
+ onEnded != null && onEnded();
58
+ isPlaying = false;
59
+ return;
60
+ };
61
+
62
+ const playNextBufferIfNotAlreadyPlaying = () => {
63
+ if (!isPlaying) {
64
+ playNextBuffer();
65
+ }
66
+ };
67
+
68
+ const playNextBuffer = () => {
69
+ if (shouldPlayWhenAudioAvailable === false) {
70
+ console.debug(
71
+ '[BufferedSpeechPlayer][playNextBuffer] Not playing any more audio because shouldPlayWhenAudioAvailable is false.',
72
+ );
73
+ // NOTE: we do not need to set isPlaying = false or call onEnded because that will be handled in the stop() function
74
+ return;
75
+ }
76
+ if (unplayedAudioBuffers.length === 0) {
77
+ console.debug(
78
+ '[BufferedSpeechPlayer][playNextBuffer] No buffers to play.',
79
+ );
80
+ if (isPlaying) {
81
+ isPlaying = false;
82
+ onEnded != null && onEnded();
83
+ }
84
+ return;
85
+ }
86
+
87
+ // If isPlaying is false, then we are starting playback fresh rather than continuing it, and should call onStarted
88
+ if (isPlaying === false) {
89
+ isPlaying = true;
90
+ onStarted != null && onStarted();
91
+ }
92
+
93
+ const source = audioContext.createBufferSource();
94
+
95
+ // Get the first unplayed buffer from the array, and remove it from the array
96
+ const buffer = unplayedAudioBuffers.shift() ?? null;
97
+ source.buffer = buffer;
98
+ console.debug(
99
+ `[BufferedSpeechPlayer] Playing buffer with ${source.buffer?.length} samples`,
100
+ );
101
+
102
+ source.connect(gainNode);
103
+ // the gain node is already connected to audioContext.destination
104
+ // source.connect(audioContext.destination);
105
+ const startTime = new Date().getTime();
106
+ source.start();
107
+ currentPlayingBufferSource = source;
108
+ // This is probably not necessary, but it doesn't hurt
109
+ isPlaying = true;
110
+
111
+ // TODO: consider changing this to a while loop to avoid deep recursion
112
+ const onThisBufferPlaybackEnded = () => {
113
+ console.debug(
114
+ `[BufferedSpeechPlayer] Buffer with ${source.buffer?.length} samples ended.`,
115
+ );
116
+ source.removeEventListener('ended', onThisBufferPlaybackEnded);
117
+ const endTime = new Date().getTime();
118
+ debug()?.playedAudio(startTime, endTime, buffer);
119
+ currentPlayingBufferSource = null;
120
+
121
+ // TODO: should we disconnect source from gain node here?
122
+ // source.disconnect(gainNode);
123
+
124
+ // We don't set isPlaying = false here because we are attempting to continue playing. It will get set to false if there are no more buffers to play
125
+ playNextBuffer();
126
+ };
127
+
128
+ source.addEventListener('ended', onThisBufferPlaybackEnded);
129
+ };
130
+
131
+ const addAudioToBuffer: AddAudioToBufferFunction = (samples, sampleRate) => {
132
+ const incomingArrayBufferChunk = audioContext.createBuffer(
133
+ // 1 channel
134
+ 1,
135
+ samples.length,
136
+ sampleRate,
137
+ );
138
+
139
+ incomingArrayBufferChunk.copyToChannel(
140
+ new Float32Array(samples),
141
+ // first channel
142
+ 0,
143
+ );
144
+
145
+ console.debug(
146
+ `[addAudioToBufferAndPlay] Adding buffer with ${incomingArrayBufferChunk.length} samples to queue.`,
147
+ );
148
+
149
+ unplayedAudioBuffers.push(incomingArrayBufferChunk);
150
+ debug()?.receivedAudio(
151
+ incomingArrayBufferChunk.length / incomingArrayBufferChunk.sampleRate,
152
+ );
153
+ const audioBuffersTableInfo = unplayedAudioBuffers.map((buffer, i) => {
154
+ return {
155
+ index: i,
156
+ duration: buffer.length / buffer.sampleRate,
157
+ samples: buffer.length,
158
+ };
159
+ });
160
+ const totalUnplayedDuration = unplayedAudioBuffers.reduce((acc, buffer) => {
161
+ return acc + buffer.length / buffer.sampleRate;
162
+ }, 0);
163
+
164
+ console.debug(
165
+ `[addAudioToBufferAndPlay] Current state of incoming audio buffers (${totalUnplayedDuration.toFixed(
166
+ 1,
167
+ )}s unplayed):`,
168
+ );
169
+ console.table(audioBuffersTableInfo);
170
+
171
+ if (shouldPlayWhenAudioAvailable) {
172
+ playNextBufferIfNotAlreadyPlaying();
173
+ }
174
+ };
175
+
176
+ return {addAudioToBuffer, setGain, stop, start};
177
+ }
streaming-react-app/src/cursorBlinkInterval.ts ADDED
@@ -0,0 +1 @@
 
 
1
+ export const CURSOR_BLINK_INTERVAL_MS = 500;
streaming-react-app/src/debug.ts ADDED
@@ -0,0 +1,257 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import {TYPING_ANIMATION_DELAY_MS} from './StreamingInterface';
2
+ import {getURLParams} from './URLParams';
3
+ import audioBuffertoWav from 'audiobuffer-to-wav';
4
+ import './StreamingInterface.css';
5
+
6
+ type StartEndTime = {
7
+ start: number;
8
+ end: number;
9
+ };
10
+
11
+ type StartEndTimeWithAudio = StartEndTime & {
12
+ float32Audio: Float32Array;
13
+ };
14
+
15
+ type Text = {
16
+ time: number;
17
+ chars: number;
18
+ };
19
+
20
+ type DebugTimings = {
21
+ receivedAudio: StartEndTime[];
22
+ playedAudio: StartEndTimeWithAudio[];
23
+ receivedText: Text[];
24
+ renderedText: StartEndTime[];
25
+ sentAudio: StartEndTimeWithAudio[];
26
+ startRenderTextTime: number | null;
27
+ startRecordingTime: number | null;
28
+ receivedAudioSampleRate: number | null;
29
+ };
30
+
31
+ function getInitialTimings(): DebugTimings {
32
+ return {
33
+ receivedAudio: [],
34
+ playedAudio: [],
35
+ receivedText: [],
36
+ renderedText: [],
37
+ sentAudio: [],
38
+ startRenderTextTime: null,
39
+ startRecordingTime: null,
40
+ receivedAudioSampleRate: null,
41
+ };
42
+ }
43
+
44
+ function downloadAudioBuffer(audioBuffer: AudioBuffer, fileName: string): void {
45
+ const wav = audioBuffertoWav(audioBuffer);
46
+ const wavBlob = new Blob([new DataView(wav)], {
47
+ type: 'audio/wav',
48
+ });
49
+ const url = URL.createObjectURL(wavBlob);
50
+ const anchor = document.createElement('a');
51
+ anchor.href = url;
52
+ anchor.target = '_blank';
53
+ anchor.download = fileName;
54
+ anchor.click();
55
+ }
56
+
57
+ // Uncomment for debugging without download
58
+ // function playAudioBuffer(audioBuffer: AudioBuffer): void {
59
+ // const audioContext = new AudioContext();
60
+ // const source = audioContext.createBufferSource();
61
+
62
+ // source.buffer = audioBuffer;
63
+ // source.connect(audioContext.destination);
64
+ // source.start();
65
+ // }
66
+
67
+ // Accumulate timings and audio / text translation samples for debugging and exporting
68
+ class DebugTimingsManager {
69
+ timings: DebugTimings = getInitialTimings();
70
+
71
+ start(): void {
72
+ this.timings = getInitialTimings();
73
+ this.timings.startRecordingTime = new Date().getTime();
74
+ }
75
+
76
+ sentAudio(event: AudioProcessingEvent): void {
77
+ const end = new Date().getTime();
78
+ const start = end - event.inputBuffer.duration * 1000;
79
+ // Copy or else buffer seems to be re-used
80
+ const float32Audio = new Float32Array(event.inputBuffer.getChannelData(0));
81
+ this.timings.sentAudio.push({
82
+ start,
83
+ end,
84
+ float32Audio,
85
+ });
86
+ }
87
+
88
+ receivedText(text: string): void {
89
+ this.timings.receivedText.push({
90
+ time: new Date().getTime(),
91
+ chars: text.length,
92
+ });
93
+ }
94
+
95
+ startRenderText(): void {
96
+ if (this.timings.startRenderTextTime == null) {
97
+ this.timings.startRenderTextTime = new Date().getTime();
98
+ }
99
+ }
100
+
101
+ endRenderText(): void {
102
+ if (this.timings.startRenderTextTime == null) {
103
+ console.warn(
104
+ 'Wrong timings of start / end rendering text. startRenderText is null',
105
+ );
106
+ return;
107
+ }
108
+
109
+ this.timings.renderedText.push({
110
+ start: this.timings.startRenderTextTime as number,
111
+ end: new Date().getTime(),
112
+ });
113
+ this.timings.startRenderTextTime = null;
114
+ }
115
+
116
+ receivedAudio(duration: number): void {
117
+ const start = new Date().getTime();
118
+ this.timings.receivedAudio.push({
119
+ start,
120
+ end: start + duration * 1000,
121
+ });
122
+ }
123
+
124
+ playedAudio(start: number, end: number, buffer: AudioBuffer | null): void {
125
+ if (buffer != null) {
126
+ if (this.timings.receivedAudioSampleRate == null) {
127
+ this.timings.receivedAudioSampleRate = buffer.sampleRate;
128
+ }
129
+ if (this.timings.receivedAudioSampleRate != buffer.sampleRate) {
130
+ console.error(
131
+ 'Sample rates of received audio are unequal, will fail to reconstruct debug audio',
132
+ this.timings.receivedAudioSampleRate,
133
+ buffer.sampleRate,
134
+ );
135
+ }
136
+ }
137
+ this.timings.playedAudio.push({
138
+ start,
139
+ end,
140
+ float32Audio:
141
+ buffer == null
142
+ ? new Float32Array()
143
+ : new Float32Array(buffer.getChannelData(0)),
144
+ });
145
+ }
146
+
147
+ getChartData() {
148
+ const columns = [
149
+ {type: 'string', id: 'Series'},
150
+ {type: 'date', id: 'Start'},
151
+ {type: 'date', id: 'End'},
152
+ ];
153
+ return [
154
+ columns,
155
+ ...this.timings.sentAudio.map((sentAudio) => [
156
+ 'Sent Audio',
157
+ new Date(sentAudio.start),
158
+ new Date(sentAudio.end),
159
+ ]),
160
+ ...this.timings.receivedAudio.map((receivedAudio) => [
161
+ 'Received Audio',
162
+ new Date(receivedAudio.start),
163
+ new Date(receivedAudio.end),
164
+ ]),
165
+ ...this.timings.playedAudio.map((playedAudio) => [
166
+ 'Played Audio',
167
+ new Date(playedAudio.start),
168
+ new Date(playedAudio.end),
169
+ ]),
170
+ // Best estimate duration by multiplying length with animation duration for each letter
171
+ ...this.timings.receivedText.map((receivedText) => [
172
+ 'Received Text',
173
+ new Date(receivedText.time),
174
+ new Date(
175
+ receivedText.time + receivedText.chars * TYPING_ANIMATION_DELAY_MS,
176
+ ),
177
+ ]),
178
+ ...this.timings.renderedText.map((renderedText) => [
179
+ 'Rendered Text',
180
+ new Date(renderedText.start),
181
+ new Date(renderedText.end),
182
+ ]),
183
+ ];
184
+ }
185
+
186
+ downloadInputAudio() {
187
+ const audioContext = new AudioContext();
188
+ const totalLength = this.timings.sentAudio.reduce((acc, cur) => {
189
+ return acc + cur?.float32Audio?.length ?? 0;
190
+ }, 0);
191
+ if (totalLength === 0) {
192
+ return;
193
+ }
194
+
195
+ const incomingArrayBuffer = audioContext.createBuffer(
196
+ 1, // 1 channel
197
+ totalLength,
198
+ audioContext.sampleRate,
199
+ );
200
+
201
+ const buffer = incomingArrayBuffer.getChannelData(0);
202
+ let i = 0;
203
+ this.timings.sentAudio.forEach((sentAudio) => {
204
+ sentAudio.float32Audio.forEach((bytes) => {
205
+ buffer[i++] = bytes;
206
+ });
207
+ });
208
+
209
+ // Play for debugging
210
+ // playAudioBuffer(incomingArrayBuffer);
211
+ downloadAudioBuffer(incomingArrayBuffer, `input_audio.wav`);
212
+ }
213
+
214
+ downloadOutputAudio() {
215
+ const playedAudio = this.timings.playedAudio;
216
+ const sampleRate = this.timings.receivedAudioSampleRate;
217
+ if (
218
+ playedAudio.length === 0 ||
219
+ this.timings.startRecordingTime == null ||
220
+ sampleRate == null
221
+ ) {
222
+ return null;
223
+ }
224
+
225
+ let previousEndTime = this.timings.startRecordingTime;
226
+ const audioArray: number[] = [];
227
+ playedAudio.forEach((audio) => {
228
+ const delta = (audio.start - previousEndTime) / 1000;
229
+ for (let i = 0; i < delta * sampleRate; i++) {
230
+ audioArray.push(0.0);
231
+ }
232
+ audio.float32Audio.forEach((bytes) => audioArray.push(bytes));
233
+ previousEndTime = audio.end;
234
+ });
235
+ const audioContext = new AudioContext();
236
+ const incomingArrayBuffer = audioContext.createBuffer(
237
+ 1, // 1 channel
238
+ audioArray.length,
239
+ sampleRate,
240
+ );
241
+
242
+ incomingArrayBuffer.copyToChannel(
243
+ new Float32Array(audioArray),
244
+ 0, // first channel
245
+ );
246
+
247
+ // Play for debugging
248
+ // playAudioBuffer(incomingArrayBuffer);
249
+ downloadAudioBuffer(incomingArrayBuffer, 'output_audio.wav');
250
+ }
251
+ }
252
+
253
+ const debugSingleton = new DebugTimingsManager();
254
+ export default function debug(): DebugTimingsManager | null {
255
+ const debugParam = getURLParams().debug;
256
+ return debugParam ? debugSingleton : null;
257
+ }
streaming-react-app/src/float32To16BitPCM.ts ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export default function float32To16BitPCM(
2
+ float32Arr: Float32Array,
3
+ ): Int16Array {
4
+ const pcm16bit = new Int16Array(float32Arr.length);
5
+ for (let i = 0; i < float32Arr.length; ++i) {
6
+ // force number in [-1,1]
7
+ const s = Math.max(-1, Math.min(1, float32Arr[i]));
8
+
9
+ /**
10
+ * convert 32 bit float to 16 bit int pcm audio
11
+ * 0x8000 = minimum int16 value, 0x7fff = maximum int16 value
12
+ */
13
+ pcm16bit[i] = s < 0 ? s * 0x8000 : s * 0x7fff;
14
+ }
15
+ return pcm16bit;
16
+ }
streaming-react-app/src/generateNewRoomID.ts ADDED
@@ -0,0 +1,60 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import {random} from 'lodash';
2
+
3
+ // const USABLE_CHARACTERS = 'BCDFGHJKMPQRTVWXY2346789';
4
+ const USABLE_CHARACTERS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
5
+ const ID_LENGTH = 4;
6
+
7
+ export function isValidRoomID(id: string | null | undefined): boolean {
8
+ if (id == null) {
9
+ return false;
10
+ }
11
+ if (id.length !== ID_LENGTH) {
12
+ return false;
13
+ }
14
+ return isValidPartialRoomID(id);
15
+ }
16
+
17
+ export function isValidPartialRoomID(roomID: string): boolean {
18
+ return (
19
+ roomID.length <= ID_LENGTH &&
20
+ roomID.split('').every((char) => USABLE_CHARACTERS.includes(char))
21
+ );
22
+ }
23
+
24
+ export default function generateNewRoomID(): string {
25
+ return Array.from(
26
+ {length: ID_LENGTH},
27
+ () => USABLE_CHARACTERS[random(USABLE_CHARACTERS.length - 1)],
28
+ ).join('');
29
+ }
30
+
31
+ export function getSequentialRoomIDForTestingGenerator(): () => string {
32
+ let counter = 0;
33
+
34
+ return function generateNextRoomID(): string {
35
+ const counterInBase: string = Number(counter)
36
+ .toString(USABLE_CHARACTERS.length)
37
+ .padStart(ID_LENGTH, '0');
38
+
39
+ if (counterInBase.length > ID_LENGTH) {
40
+ throw new Error(
41
+ 'Ran out of unique room IDs from the sequential generator',
42
+ );
43
+ }
44
+
45
+ const result = counterInBase
46
+ .split('')
47
+ .map(
48
+ (digit) => USABLE_CHARACTERS[parseInt(digit, USABLE_CHARACTERS.length)],
49
+ )
50
+ .join('');
51
+
52
+ counter++;
53
+
54
+ return result;
55
+ };
56
+ }
57
+
58
+ // const generator = getSequentialRoomIDForTestingGenerator();
59
+
60
+ // Array.from({length: 200}, () => console.log(generator()));
streaming-react-app/src/getParamFlag.ts ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type {URLParamNames} from './types/URLParamsTypes';
2
+
3
+ export function getBooleanParamFlag(
4
+ flag: URLParamNames,
5
+ defaultValue?: boolean,
6
+ ): boolean {
7
+ const paramFlagValue = getBooleanParamFlagWithoutDefault(flag);
8
+
9
+ if (paramFlagValue == null) {
10
+ // The default value for paramFlags is false, unless they explicitly provide a
11
+ // defaultValue via the config
12
+ return defaultValue ?? false;
13
+ }
14
+
15
+ return paramFlagValue;
16
+ }
17
+
18
+ export function getBooleanParamFlagWithoutDefault(
19
+ flag: URLParamNames,
20
+ ): boolean | null {
21
+ const urlParams = new URLSearchParams(window.location.search);
22
+
23
+ if (urlParams.get(flag) == null) {
24
+ return null;
25
+ }
26
+
27
+ return urlParams.get(flag) !== '0';
28
+ }
29
+
30
+ export function getStringParamFlag(
31
+ flag: URLParamNames,
32
+ defaultValue?: string,
33
+ ): string | null {
34
+ const urlParams = new URLSearchParams(window.location.search);
35
+
36
+ const param = urlParams.get(flag);
37
+
38
+ return param ?? defaultValue ?? null;
39
+ }
streaming-react-app/src/getTranslationSentencesFromReceivedData.ts ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import {ServerTextData, TranslationSentences} from './types/StreamingTypes';
2
+
3
+ export default function getTranslationSentencesFromReceivedData(
4
+ receivedData: Array<ServerTextData>,
5
+ ): TranslationSentences {
6
+ return receivedData
7
+ .reduce(
8
+ (acc, data) => {
9
+ // TODO: Add special handling if the payload starts/ends with an apostrophe?
10
+ const newAcc = [
11
+ ...acc.slice(0, -1),
12
+ acc[acc.length - 1].trim() + ' ' + data.payload,
13
+ ];
14
+ if (data.eos) {
15
+ newAcc.push('');
16
+ }
17
+
18
+ return newAcc;
19
+ },
20
+ [''],
21
+ )
22
+ .filter((s) => s.trim().length !== 0);
23
+ }
streaming-react-app/src/index.css ADDED
File without changes
streaming-react-app/src/isScrolledToDocumentBottom.ts ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export default function isScrolledToDocumentBottom(
2
+ bufferPx: number = 0,
3
+ ): boolean {
4
+ if (
5
+ window.innerHeight + window.scrollY >=
6
+ document.body.offsetHeight - bufferPx
7
+ ) {
8
+ return true;
9
+ }
10
+ return false;
11
+ }
streaming-react-app/src/main.tsx ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react';
2
+ import ReactDOM from 'react-dom/client';
3
+ import App from './App.tsx';
4
+ import './index.css';
5
+
6
+ ReactDOM.createRoot(document.getElementById('root')!).render(
7
+ <React.StrictMode>
8
+ <App />
9
+ </React.StrictMode>,
10
+ );
streaming-react-app/src/react-xr/Button.tsx ADDED
@@ -0,0 +1,117 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import {useRef, useEffect} from 'react';
2
+ import * as THREE from 'three';
3
+ import {extend} from '@react-three/fiber';
4
+ import ThreeMeshUI from 'three-mesh-ui';
5
+ import ThreeMeshUIText, {ThreeMeshUITextType} from './ThreeMeshUIText';
6
+ import {Interactive} from '@react-three/xr';
7
+
8
+ /**
9
+ * Using `?url` at the end of this import tells vite this is a static asset, and
10
+ * provides us a URL to the hashed version of the file when the project is built.
11
+ * See: https://vitejs.dev/guide/assets.html#explicit-url-imports
12
+ */
13
+ import robotoFontFamilyJson from '../assets/RobotoMono-Regular-msdf.json?url';
14
+ import robotoFontTexture from '../assets/RobotoMono-Regular.png';
15
+
16
+ extend(ThreeMeshUI);
17
+
18
+ /**
19
+ * Button component that renders as a three-mesh-ui block
20
+ */
21
+ export default function Button({
22
+ onClick,
23
+ content,
24
+ width,
25
+ height,
26
+ fontSize,
27
+ borderRadius,
28
+ padding,
29
+ }) {
30
+ const button = useRef<JSX.IntrinsicElements['block']>();
31
+ const textRef = useRef<ThreeMeshUITextType>();
32
+
33
+ useEffect(() => {
34
+ if (textRef.current != null) {
35
+ textRef.current.set({content});
36
+ }
37
+ }, [textRef, content]);
38
+
39
+ useEffect(() => {
40
+ if (!button.current) {
41
+ return;
42
+ }
43
+ button.current.setupState({
44
+ state: 'hovered',
45
+ attributes: {
46
+ offset: 0.002,
47
+ backgroundColor: new THREE.Color(0x607b8f),
48
+ fontColor: new THREE.Color(0xffffff),
49
+ },
50
+ });
51
+ button.current.setupState({
52
+ state: 'idle',
53
+ attributes: {
54
+ offset: 0.001,
55
+ backgroundColor: new THREE.Color(0x465a69),
56
+ fontColor: new THREE.Color(0xffffff),
57
+ },
58
+ });
59
+ button.current.setupState({
60
+ state: 'selected',
61
+ attributes: {
62
+ offset: 0.005,
63
+ backgroundColor: new THREE.Color(0x000000),
64
+ fontColor: new THREE.Color(0xffffff),
65
+ },
66
+ });
67
+ button.current.setState('idle');
68
+ }, []);
69
+
70
+ const args = [
71
+ {
72
+ width,
73
+ height,
74
+ fontSize,
75
+ padding,
76
+ justifyContent: 'end',
77
+ textAlign: 'center',
78
+ alignItems: 'center',
79
+ borderRadius,
80
+ fontFamily: robotoFontFamilyJson,
81
+ fontTexture: robotoFontTexture,
82
+ backgroundOpacity: 1,
83
+ backgroundColor: new THREE.Color(0x779092),
84
+ fontColor: new THREE.Color(0x000000),
85
+ },
86
+ ];
87
+
88
+ return (
89
+ <Interactive
90
+ // These are for XR mode
91
+ onSelect={() => {
92
+ onClick();
93
+ }}
94
+ onHover={() => button.current.setState('hovered')}
95
+ onBlur={() => button.current.setState('idle')}
96
+ onSelectStart={() => button.current.setState('selected')}
97
+ onSelectEnd={() => button.current.setState('idle')}>
98
+ <block
99
+ // These are for non-XR modes
100
+ onPointerEnter={() => button.current.setState('hovered')}
101
+ onPointerLeave={() => button.current.setState('idle')}
102
+ onPointerDown={() => button.current.setState('selected')}
103
+ onPointerUp={() => {
104
+ button.current.setState('hovered');
105
+ onClick();
106
+ }}>
107
+ <block args={args} ref={button}>
108
+ <ThreeMeshUIText
109
+ ref={textRef}
110
+ fontColor={new THREE.Color(0xffffff)}
111
+ content={content}
112
+ />
113
+ </block>
114
+ </block>
115
+ </Interactive>
116
+ );
117
+ }
streaming-react-app/src/react-xr/Colors.ts ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ import * as THREE from 'three';
2
+
3
+ export const WHITE = new THREE.Color('#FFFFFF');
4
+ export const BLACK = new THREE.Color('#000000');
5
+ export const RED = new THREE.Color('red');
6
+ export const BLUE = new THREE.Color('blue');
streaming-react-app/src/react-xr/MovementController.tsx ADDED
@@ -0,0 +1,64 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import {useRef} from 'react';
2
+ import {useFrame} from '@react-three/fiber';
3
+ import {useController, useXR} from '@react-three/xr';
4
+ import * as THREE from 'three';
5
+
6
+ const USE_HORIZONTAL = true;
7
+ const USE_VERTICAL = true;
8
+ const USE_ROTATION = true;
9
+ const HORIZONTAL_AXIS = 2;
10
+ const VERTICAL_AXIS = 3;
11
+ const ROTATION_AXIS = 2;
12
+ const SENSITIVITY = 0.05;
13
+ const DEADZONE = 0.05;
14
+
15
+ /**
16
+ * Component to add into the ThreeJS canvas that reads controller (Quest) inputs to change camera position
17
+ */
18
+ export default function MovementController() {
19
+ const xr = useXR();
20
+ const controller = useController('right');
21
+ const forward = useRef(new THREE.Vector3());
22
+ const horizontal = useRef(new THREE.Vector3());
23
+
24
+ useFrame(() => {
25
+ const player = xr.player;
26
+ const camera = xr.player.children[0];
27
+ const cameraMatrix = camera.matrixWorld.elements;
28
+ forward.current
29
+ .set(-cameraMatrix[8], -cameraMatrix[9], -cameraMatrix[10])
30
+ .normalize();
31
+
32
+ const axes = controller?.inputSource?.gamepad?.axes ?? [0, 0, 0, 0];
33
+
34
+ if (USE_HORIZONTAL) {
35
+ horizontal.current.copy(forward.current);
36
+ horizontal.current.cross(camera.up).normalize();
37
+
38
+ player.position.add(
39
+ horizontal.current.multiplyScalar(
40
+ (Math.abs(axes[HORIZONTAL_AXIS]) > DEADZONE
41
+ ? axes[HORIZONTAL_AXIS]
42
+ : 0) * SENSITIVITY,
43
+ ),
44
+ );
45
+ }
46
+
47
+ if (USE_VERTICAL) {
48
+ player.position.add(
49
+ forward.current.multiplyScalar(
50
+ (Math.abs(axes[VERTICAL_AXIS]) > DEADZONE ? axes[VERTICAL_AXIS] : 0) *
51
+ SENSITIVITY,
52
+ ),
53
+ );
54
+ }
55
+
56
+ if (USE_ROTATION) {
57
+ player.rotation.y -=
58
+ (Math.abs(axes[ROTATION_AXIS]) > DEADZONE ? axes[ROTATION_AXIS] : 0) *
59
+ SENSITIVITY;
60
+ }
61
+ });
62
+
63
+ return <></>;
64
+ }
streaming-react-app/src/react-xr/Playground.tsx ADDED
@@ -0,0 +1,133 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * EXPERIMENTAL components to play around with but not officially use in the demo while
3
+ * we develop.
4
+ */
5
+ import {useEffect, useState} from 'react';
6
+ import {Object3DNode, extend} from '@react-three/fiber';
7
+ import ThreeMeshUI from 'three-mesh-ui';
8
+
9
+ import {} from '@react-three/xr';
10
+ import {Sparkles, Shadow} from '@react-three/drei';
11
+
12
+ // import FontImage from './assets/Roboto-msdf.png';
13
+ import {FontLoader} from 'three/examples/jsm/loaders/FontLoader.js';
14
+ import {TextGeometry} from 'three/examples/jsm/geometries/TextGeometry.js';
15
+ import ThreeMeshUIText from './ThreeMeshUIText';
16
+ import {ContactShadows, BakeShadows} from '@react-three/drei';
17
+
18
+ extend({TextGeometry});
19
+ extend(ThreeMeshUI);
20
+
21
+ declare module '@react-three/fiber' {
22
+ interface ThreeElements {
23
+ textGeometry: Object3DNode<TextGeometry, typeof TextGeometry>;
24
+ }
25
+ }
26
+
27
+ // This is for textGeometry.. not using three-mesh-ui to display text
28
+ export function TitleMesh() {
29
+ const font = new FontLoader().parse();
30
+ console.log('font', font);
31
+ const [text, setText] = useState('Text');
32
+
33
+ useEffect(() => {
34
+ setTimeout(() => {
35
+ setText(text + ' more ');
36
+ console.log('adding more tex..', text);
37
+ }, 1000);
38
+ }, [text]);
39
+
40
+ return (
41
+ <mesh>
42
+ <textGeometry args={[text, {font, size: 5, height: 1}]} />
43
+ <meshPhysicalMaterial attach={'material'} color={'white'} />
44
+ </mesh>
45
+ );
46
+ }
47
+
48
+ export function Sphere({
49
+ size = 1,
50
+ amount = 50,
51
+ color = 'white',
52
+ emissive,
53
+ ...props
54
+ }) {
55
+ return (
56
+ <mesh {...props}>
57
+ <sphereGeometry args={[size, 64, 64]} />
58
+ <meshPhysicalMaterial
59
+ roughness={0}
60
+ color={color}
61
+ emissive={emissive || color}
62
+ envMapIntensity={0.2}
63
+ />
64
+ <Sparkles count={amount} scale={size * 2} size={6} speed={0.4} />
65
+ <Shadow
66
+ rotation={[-Math.PI / 2, 0, 0]}
67
+ scale={size}
68
+ position={[0, -size, 0]}
69
+ color={emissive}
70
+ opacity={0.5}
71
+ />
72
+ </mesh>
73
+ );
74
+ }
75
+
76
+ export function Title({accentColor}) {
77
+ return (
78
+ <block
79
+ args={[
80
+ {
81
+ width: 1,
82
+ height: 0.25,
83
+ backgroundOpacity: 0,
84
+ justifyContent: 'center',
85
+ },
86
+ ]}>
87
+ <ThreeMeshUIText content={'Hello '} />
88
+ <ThreeMeshUIText content={'world!'} args={[{fontColor: accentColor}]} />
89
+ </block>
90
+ );
91
+ }
92
+
93
+ export function RandomComponents() {
94
+ return (
95
+ <>
96
+ <color args={['#eee']} attach={'background'} />
97
+ <Sphere
98
+ color="white"
99
+ amount={50}
100
+ emissive="green"
101
+ glow="lightgreen"
102
+ position={[1, 1, -1]}
103
+ />
104
+ <Sphere
105
+ color="white"
106
+ amount={30}
107
+ emissive="purple"
108
+ glow="#ff90f0"
109
+ size={0.5}
110
+ position={[-1.5, 0.5, -2]}
111
+ />
112
+ <Sphere
113
+ color="lightpink"
114
+ amount={20}
115
+ emissive="orange"
116
+ glow="#ff9f50"
117
+ size={0.25}
118
+ position={[-1, 0.25, 1]}
119
+ />
120
+ <ContactShadows
121
+ renderOrder={2}
122
+ color="black"
123
+ resolution={1024}
124
+ frames={1}
125
+ scale={10}
126
+ blur={1.5}
127
+ opacity={0.65}
128
+ far={0.5}
129
+ />
130
+ <BakeShadows />
131
+ </>
132
+ );
133
+ }
streaming-react-app/src/react-xr/TextBlocks.tsx ADDED
@@ -0,0 +1,235 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import {JSX, useEffect, useRef, useState} from 'react';
2
+ import robotoFontFamilyJson from '../assets/RobotoMono-Regular-msdf.json?url';
3
+ import robotoFontTexture from '../assets/RobotoMono-Regular.png';
4
+ import ThreeMeshUIText, {ThreeMeshUITextType} from './ThreeMeshUIText';
5
+ import {getURLParams} from '../URLParams';
6
+ import {CURSOR_BLINK_INTERVAL_MS} from '../cursorBlinkInterval';
7
+
8
+ const NUM_LINES = 3;
9
+
10
+ export const CHARS_PER_LINE = 37;
11
+ const MAX_WIDTH = 0.89;
12
+ const CHAR_WIDTH = 0.0235;
13
+ const Y_COORD_START = -0.38;
14
+ const Z_COORD = -1.3;
15
+ const LINE_HEIGHT = 0.062;
16
+ const BLOCK_SPACING = 0.02;
17
+ const FONT_SIZE = 0.038;
18
+
19
+ const SCROLL_Y_DELTA = 0.001;
20
+
21
+ // Overlay an extra block for padding due to inflexibilities of native padding
22
+ const OFFSET = 0.01;
23
+ const OFFSET_WIDTH = OFFSET * 3;
24
+
25
+ type Props = {
26
+ content: string;
27
+ // The actual position or end position when animating
28
+ y: number;
29
+ // The start position when animating
30
+ startY: number;
31
+ width: number;
32
+ height: number;
33
+ textOpacity: number;
34
+ backgroundOpacity: number;
35
+ // Use this to keep track of sentence + line position for animation
36
+ index: string;
37
+ enableAnimation: boolean;
38
+ };
39
+
40
+ function TextBlock({
41
+ content,
42
+ y,
43
+ startY,
44
+ width,
45
+ height,
46
+ textOpacity,
47
+ backgroundOpacity,
48
+ index,
49
+ enableAnimation,
50
+ }: Props) {
51
+ const [scrollY, setScrollY] = useState<number>(y);
52
+
53
+ // We are reusing text blocks so this keeps track of when we changed rows so we can restart animation
54
+ const lastIndex = useRef<string>(index);
55
+ useEffect(() => {
56
+ if (index != lastIndex.current) {
57
+ lastIndex.current = index;
58
+ enableAnimation && setScrollY(startY);
59
+ } else if (scrollY < y) {
60
+ setScrollY((prev) => prev + SCROLL_Y_DELTA);
61
+ }
62
+ }, [enableAnimation, index, scrollY, setScrollY, startY, y]);
63
+
64
+ // This is needed to update text content (doesn't work if we just update the content prop)
65
+ const textRef = useRef<ThreeMeshUITextType>();
66
+ useEffect(() => {
67
+ if (textRef.current != null) {
68
+ textRef.current.set({content});
69
+ }
70
+ }, [content, textRef, y, startY]);
71
+
72
+ // Width starts from 0 and goes 1/2 in each direction
73
+ const xPosition = width / 2 - MAX_WIDTH / 2 + OFFSET_WIDTH;
74
+ return (
75
+ <>
76
+ <block
77
+ args={[
78
+ {
79
+ backgroundOpacity,
80
+ width: width + OFFSET_WIDTH,
81
+ height: height,
82
+ borderRadius: 0,
83
+ },
84
+ ]}
85
+ position={[-OFFSET_WIDTH + xPosition, scrollY, Z_COORD]}></block>
86
+ <block
87
+ args={[{padding: 0, backgroundOpacity: 0, width, height}]}
88
+ position={[xPosition, scrollY + OFFSET, Z_COORD]}>
89
+ <block
90
+ args={[
91
+ {
92
+ width,
93
+ height,
94
+ fontSize: FONT_SIZE,
95
+ textAlign: 'left',
96
+ backgroundOpacity: 0,
97
+ // TODO: support more language charsets
98
+ // This renders using MSDF format supported in WebGL. Renderable characters are defined in the "charset" json
99
+ // Currently supports most default keyboard inputs but this would exclude many non latin charset based languages.
100
+ // You can use https://msdf-bmfont.donmccurdy.com/ for easily generating these files
101
+ // fontFamily: '/src/assets/Roboto-msdf.json',
102
+ // fontTexture: '/src/assets/Roboto-msdf.png'
103
+ fontFamily: robotoFontFamilyJson,
104
+ fontTexture: robotoFontTexture,
105
+ },
106
+ ]}>
107
+ <ThreeMeshUIText ref={textRef} content="" fontOpacity={textOpacity} />
108
+ </block>
109
+ </block>
110
+ </>
111
+ );
112
+ }
113
+
114
+ // Background behind the text so it covers any missing spaces
115
+ function TranscriptionPanel() {
116
+ const panelHeight = LINE_HEIGHT * NUM_LINES + 2 * BLOCK_SPACING + 2 * OFFSET;
117
+ const xPosition = OFFSET_WIDTH;
118
+ return (
119
+ <block
120
+ args={[
121
+ {
122
+ backgroundOpacity: 1,
123
+ width:
124
+ MAX_WIDTH * ((CHARS_PER_LINE + 2) / CHARS_PER_LINE) +
125
+ 2 * OFFSET_WIDTH,
126
+ height: panelHeight,
127
+ borderRadius: 0,
128
+ },
129
+ ]}
130
+ position={[
131
+ -OFFSET + xPosition,
132
+ Y_COORD_START + panelHeight / 2 - 2 * OFFSET,
133
+ Z_COORD,
134
+ ]}></block>
135
+ );
136
+ }
137
+
138
+ export default function TextBlocks({
139
+ sentences,
140
+ blinkCursor,
141
+ }: {
142
+ sentences: string[][];
143
+ blinkCursor: boolean;
144
+ }) {
145
+ const showTranscriptionPanel =
146
+ getURLParams().ARTranscriptionType === 'lines_with_background';
147
+ const textBlocks: JSX.Element[] = [];
148
+
149
+ const [cursorBlinkOn, setCursorBlinkOn] = useState(false);
150
+ useEffect(() => {
151
+ if (blinkCursor) {
152
+ const interval = setInterval(() => {
153
+ setCursorBlinkOn((prev) => !prev);
154
+ }, CURSOR_BLINK_INTERVAL_MS);
155
+
156
+ return () => clearInterval(interval);
157
+ } else {
158
+ setCursorBlinkOn(false);
159
+ }
160
+ }, [blinkCursor]);
161
+
162
+ // Start from bottom and populate most recent sentences by line until we fill max lines.
163
+ let currentY = Y_COORD_START;
164
+ for (let i = sentences.length - 1; i >= 0; i--) {
165
+ const sentenceLines = sentences[i];
166
+ for (let j = sentenceLines.length - 1; j >= 0; j--) {
167
+ if (textBlocks.length == NUM_LINES) {
168
+ if (showTranscriptionPanel) {
169
+ textBlocks.push(<TranscriptionPanel key={textBlocks.length} />);
170
+ }
171
+ return textBlocks;
172
+ }
173
+
174
+ const isBottomSentence = i === sentences.length - 1;
175
+ const isBottomLine = isBottomSentence && textBlocks.length === 0;
176
+ const y = currentY + LINE_HEIGHT / 2;
177
+ let textBlockLine = sentenceLines[j];
178
+ const numChars = textBlockLine.length;
179
+
180
+ if (cursorBlinkOn && isBottomLine) {
181
+ textBlockLine = textBlockLine + '|';
182
+ }
183
+
184
+ // Accounting for potential cursor for block width (the +1)
185
+ const blockWidth =
186
+ (numChars + (isBottomLine ? 1.1 : 0) + (numChars < 10 ? 1 : 0)) *
187
+ CHAR_WIDTH;
188
+ const textOpacity = 1 - 0.1 * textBlocks.length;
189
+ textBlocks.push(
190
+ <TextBlock
191
+ key={textBlocks.length}
192
+ y={y}
193
+ startY={currentY}
194
+ index={`${sentences.length - i},${j}`}
195
+ textOpacity={textOpacity}
196
+ backgroundOpacity={0.98}
197
+ height={LINE_HEIGHT}
198
+ width={blockWidth}
199
+ // content={"BLOCK " + textBlocks.length + ": " + content}
200
+ content={textBlockLine}
201
+ enableAnimation={!isBottomLine}
202
+ />,
203
+ );
204
+
205
+ currentY = y + LINE_HEIGHT / 2;
206
+ }
207
+ currentY += showTranscriptionPanel ? BLOCK_SPACING / 3 : BLOCK_SPACING;
208
+ }
209
+
210
+ const numRemainingBlocks = textBlocks.length - NUM_LINES;
211
+ if (numRemainingBlocks > 0) {
212
+ Array.from({length: numRemainingBlocks}).forEach(() => {
213
+ // Push in non display blocks because mesh UI crashes if elements are add / removed from screen.
214
+ textBlocks.push(
215
+ <TextBlock
216
+ key={textBlocks.length}
217
+ y={Y_COORD_START}
218
+ startY={0}
219
+ index="0,0"
220
+ textOpacity={0}
221
+ backgroundOpacity={0}
222
+ enableAnimation={false}
223
+ width={MAX_WIDTH}
224
+ height={LINE_HEIGHT}
225
+ content=""
226
+ />,
227
+ );
228
+ });
229
+ }
230
+
231
+ if (showTranscriptionPanel) {
232
+ textBlocks.push(<TranscriptionPanel key={textBlocks.length} />);
233
+ }
234
+ return textBlocks;
235
+ }
streaming-react-app/src/react-xr/ThreeMeshUIText.tsx ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import {extend} from '@react-three/fiber';
2
+ import {forwardRef} from 'react';
3
+ import ThreeMeshUI, {TextOptions} from 'three-mesh-ui';
4
+
5
+ extend(ThreeMeshUI);
6
+
7
+ /**
8
+ * Hacky but component that wraps <text> since this has typescript issues because it collides with
9
+ * the native <text> SVG element. Simple enough so abstracting it away in this file
10
+ * so it could be used in other places with low risk. e.g:
11
+ * <ThreeMeshUIText content="Hello" />
12
+ */
13
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
14
+ export type ThreeMeshUITextType = any;
15
+
16
+ const ThreeMeshUIText = forwardRef<ThreeMeshUITextType, TextOptions>(
17
+ function ThreeMeshUIText(props, ref) {
18
+ return <text {...props} ref={ref} />;
19
+ },
20
+ );
21
+
22
+ export default ThreeMeshUIText;
streaming-react-app/src/react-xr/XRConfig.tsx ADDED
@@ -0,0 +1,449 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import {useCallback, useEffect, useRef, useState} from 'react';
2
+ import {
3
+ Canvas,
4
+ createPortal,
5
+ extend,
6
+ useFrame,
7
+ useThree,
8
+ } from '@react-three/fiber';
9
+ import ThreeMeshUI from 'three-mesh-ui';
10
+
11
+ import {ARButton, XR, Hands, XREvent} from '@react-three/xr';
12
+
13
+ import {TextGeometry} from 'three/examples/jsm/geometries/TextGeometry.js';
14
+ import {TranslationSentences} from '../types/StreamingTypes';
15
+ import Button from './Button';
16
+ import {RoomState} from '../types/RoomState';
17
+ import ThreeMeshUIText, {ThreeMeshUITextType} from './ThreeMeshUIText';
18
+ import {BLACK, WHITE} from './Colors';
19
+
20
+ /**
21
+ * Using `?url` at the end of this import tells vite this is a static asset, and
22
+ * provides us a URL to the hashed version of the file when the project is built.
23
+ * See: https://vitejs.dev/guide/assets.html#explicit-url-imports
24
+ */
25
+ import robotoFontFamilyJson from '../assets/RobotoMono-Regular-msdf.json?url';
26
+ import robotoFontTexture from '../assets/RobotoMono-Regular.png';
27
+ import {getURLParams} from '../URLParams';
28
+ import TextBlocks, {CHARS_PER_LINE} from './TextBlocks';
29
+ import {BufferedSpeechPlayer} from '../createBufferedSpeechPlayer';
30
+ import {CURSOR_BLINK_INTERVAL_MS} from '../cursorBlinkInterval';
31
+
32
+ // Adds on react JSX for add-on libraries to react-three-fiber
33
+ extend(ThreeMeshUI);
34
+ extend({TextGeometry});
35
+
36
+ async function fetchSupportedCharSet(): Promise<Set<string>> {
37
+ try {
38
+ const response = await fetch(robotoFontFamilyJson);
39
+ const fontFamily = await response.json();
40
+
41
+ return new Set(fontFamily.info.charset);
42
+ } catch (e) {
43
+ console.error('Failed to fetch supported XR charset', e);
44
+ return new Set();
45
+ }
46
+ }
47
+
48
+ let supportedCharSet = new Set();
49
+ fetchSupportedCharSet().then((result) => (supportedCharSet = result));
50
+
51
+ // This component wraps any children so it is positioned relative to the camera, rather than from the origin
52
+ function CameraLinkedObject({children}) {
53
+ const camera = useThree((state) => state.camera);
54
+ return createPortal(<>{children}</>, camera);
55
+ }
56
+
57
+ function ThreeMeshUIComponents({
58
+ translationSentences,
59
+ skipARIntro,
60
+ roomState,
61
+ animateTextDisplay,
62
+ }: XRConfigProps & {skipARIntro: boolean}) {
63
+ // The "loop" for re-rendering required for threemeshUI
64
+ useFrame(() => {
65
+ ThreeMeshUI.update();
66
+ });
67
+ const [started, setStarted] = useState<boolean>(skipARIntro);
68
+ return (
69
+ <>
70
+ <CameraLinkedObject>
71
+ {getURLParams().ARTranscriptionType === 'single_block' ? (
72
+ <TranscriptPanelSingleBlock
73
+ started={started}
74
+ animateTextDisplay={animateTextDisplay}
75
+ roomState={roomState}
76
+ translationSentences={translationSentences}
77
+ />
78
+ ) : (
79
+ <TranscriptPanelBlocks
80
+ animateTextDisplay={animateTextDisplay}
81
+ translationSentences={translationSentences}
82
+ />
83
+ )}
84
+ {skipARIntro ? null : (
85
+ <IntroPanel started={started} setStarted={setStarted} />
86
+ )}
87
+ </CameraLinkedObject>
88
+ </>
89
+ );
90
+ }
91
+
92
+ // Original UI that just uses a single block to render 6 lines in a panel
93
+ function TranscriptPanelSingleBlock({
94
+ animateTextDisplay,
95
+ started,
96
+ translationSentences,
97
+ roomState,
98
+ }: {
99
+ animateTextDisplay: boolean;
100
+ started: boolean;
101
+ translationSentences: TranslationSentences;
102
+ roomState: RoomState | null;
103
+ }) {
104
+ const textRef = useRef<ThreeMeshUITextType>();
105
+ const [didReceiveTranslationSentences, setDidReceiveTranslationSentences] =
106
+ useState(false);
107
+
108
+ const hasActiveTranscoders = (roomState?.activeTranscoders ?? 0) > 0;
109
+
110
+ const [cursorBlinkOn, setCursorBlinkOn] = useState(false);
111
+
112
+ // Normally we don't setState in render, but here we need to for computed state, and this if statement assures it won't loop infinitely
113
+ if (!didReceiveTranslationSentences && translationSentences.length > 0) {
114
+ setDidReceiveTranslationSentences(true);
115
+ }
116
+
117
+ const width = 1;
118
+ const height = 0.3;
119
+ const fontSize = 0.03;
120
+
121
+ useEffect(() => {
122
+ if (animateTextDisplay && hasActiveTranscoders) {
123
+ const interval = setInterval(() => {
124
+ setCursorBlinkOn((prev) => !prev);
125
+ }, CURSOR_BLINK_INTERVAL_MS);
126
+
127
+ return () => clearInterval(interval);
128
+ } else {
129
+ setCursorBlinkOn(false);
130
+ }
131
+ }, [animateTextDisplay, hasActiveTranscoders]);
132
+
133
+ useEffect(() => {
134
+ if (textRef.current != null) {
135
+ const initialPrompt =
136
+ 'Welcome to the presentation. We are excited to share with you the work we have been doing... Our model can now translate languages in less than 2 second latency.';
137
+ // These are rough ratios based on spot checking
138
+ const maxLines = 6;
139
+ const charsPerLine = 55;
140
+
141
+ const transcriptSentences: string[] = didReceiveTranslationSentences
142
+ ? translationSentences
143
+ : [initialPrompt];
144
+
145
+ // The transcript is an array of sentences. For each sentence we break this down into an array of words per line.
146
+ // This is needed so we can "scroll" through without changing the order of words in the transcript
147
+ const linesToDisplay = transcriptSentences.flatMap((sentence, idx) => {
148
+ const blinkingCursor =
149
+ cursorBlinkOn && idx === transcriptSentences.length - 1 ? '|' : ' ';
150
+ const words = sentence.concat(blinkingCursor).split(/\s+/);
151
+ // Here we break each sentence up with newlines so all words per line fit within the panel
152
+ return words.reduce(
153
+ (wordChunks, currentWord) => {
154
+ const filteredWord = [...currentWord]
155
+ .filter((c) => {
156
+ if (supportedCharSet.has(c)) {
157
+ return true;
158
+ }
159
+ console.error(
160
+ `Unsupported char ${c} - make sure this is supported in the font family msdf file`,
161
+ );
162
+ return false;
163
+ })
164
+ .join('');
165
+ const lastLineSoFar = wordChunks[wordChunks.length - 1];
166
+ const charCount = lastLineSoFar.length + filteredWord.length + 1;
167
+ if (charCount <= charsPerLine) {
168
+ wordChunks[wordChunks.length - 1] =
169
+ lastLineSoFar + ' ' + filteredWord;
170
+ } else {
171
+ wordChunks.push(filteredWord);
172
+ }
173
+ return wordChunks;
174
+ },
175
+ [''],
176
+ );
177
+ });
178
+
179
+ // Only keep the last maxLines so new text keeps scrolling up from the bottom
180
+ linesToDisplay.splice(0, linesToDisplay.length - maxLines);
181
+ textRef.current.set({content: linesToDisplay.join('\n')});
182
+ }
183
+ }, [
184
+ translationSentences,
185
+ textRef,
186
+ didReceiveTranslationSentences,
187
+ cursorBlinkOn,
188
+ ]);
189
+
190
+ const opacity = started ? 1 : 0;
191
+ return (
192
+ <block
193
+ args={[{padding: 0.05, backgroundOpacity: opacity}]}
194
+ position={[0, -0.4, -1.3]}>
195
+ <block
196
+ args={[
197
+ {
198
+ width,
199
+ height,
200
+ fontSize,
201
+ textAlign: 'left',
202
+ backgroundOpacity: opacity,
203
+ // TODO: support more language charsets
204
+ // This renders using MSDF format supported in WebGL. Renderable characters are defined in the "charset" json
205
+ // Currently supports most default keyboard inputs but this would exclude many non latin charset based languages.
206
+ // You can use https://msdf-bmfont.donmccurdy.com/ for easily generating these files
207
+ // fontFamily: '/src/assets/Roboto-msdf.json',
208
+ // fontTexture: '/src/assets/Roboto-msdf.png'
209
+ fontFamily: robotoFontFamilyJson,
210
+ fontTexture: robotoFontTexture,
211
+ },
212
+ ]}>
213
+ <ThreeMeshUIText
214
+ ref={textRef}
215
+ content={'Transcript'}
216
+ fontOpacity={opacity}
217
+ />
218
+ </block>
219
+ </block>
220
+ );
221
+ }
222
+
223
+ // Splits up the lines into separate blocks to treat each one separately.
224
+ // This allows changing of opacity, animating per line, changing height / width per line etc
225
+ function TranscriptPanelBlocks({
226
+ animateTextDisplay,
227
+ translationSentences,
228
+ }: {
229
+ animateTextDisplay: boolean;
230
+ translationSentences: TranslationSentences;
231
+ }) {
232
+ const [didReceiveTranslationSentences, setDidReceiveTranslationSentences] =
233
+ // Currently causing issues with displaying dummy text, skip over
234
+ useState(false);
235
+
236
+ // Normally we don't setState in render, but here we need to for computed state, and this if statement assures it won't loop infinitely
237
+ if (!didReceiveTranslationSentences && translationSentences.length > 0) {
238
+ setDidReceiveTranslationSentences(true);
239
+ }
240
+
241
+ const initialPrompt = 'Listening...';
242
+ const transcriptSentences: string[] = didReceiveTranslationSentences
243
+ ? translationSentences
244
+ : [initialPrompt];
245
+
246
+ // The transcript is an array of sentences. For each sentence we break this down into an array of words per line.
247
+ // This is needed so we can "scroll" through without changing the order of words in the transcript
248
+ const sentenceLines = transcriptSentences.map((sentence) => {
249
+ const words = sentence.split(/\s+/);
250
+ // Here we break each sentence up with newlines so all words per line fit within the panel
251
+ return words.reduce(
252
+ (wordChunks, currentWord) => {
253
+ const filteredWord = [...currentWord]
254
+ .filter((c) => {
255
+ if (supportedCharSet.has(c)) {
256
+ return true;
257
+ }
258
+ console.error(
259
+ `Unsupported char ${c} - make sure this is supported in the font family msdf file`,
260
+ );
261
+ return false;
262
+ })
263
+ .join('');
264
+ const lastLineSoFar = wordChunks[wordChunks.length - 1];
265
+ const charCount = lastLineSoFar.length + filteredWord.length + 1;
266
+ if (charCount <= CHARS_PER_LINE) {
267
+ wordChunks[wordChunks.length - 1] =
268
+ lastLineSoFar + ' ' + filteredWord;
269
+ } else {
270
+ wordChunks.push(filteredWord);
271
+ }
272
+ return wordChunks;
273
+ },
274
+ [''],
275
+ );
276
+ });
277
+ return (
278
+ <TextBlocks sentences={sentenceLines} blinkCursor={animateTextDisplay} />
279
+ );
280
+ }
281
+
282
+ function IntroPanel({started, setStarted}) {
283
+ const width = 0.5;
284
+ const height = 0.4;
285
+ const padding = 0.03;
286
+
287
+ // Kind of hacky but making the panel disappear by moving it completely off the camera view.
288
+ // If we try to remove elements we end up throwing and stopping the experience
289
+ // opacity=0 also runs into weird bugs where not everything is invisible
290
+ const xCoordinate = started ? 1000000 : 0;
291
+
292
+ const commonArgs = {
293
+ backgroundColor: WHITE,
294
+ width,
295
+ height,
296
+ padding,
297
+ backgroundOpacity: 1,
298
+ textAlign: 'center',
299
+ fontFamily: robotoFontFamilyJson,
300
+ fontTexture: robotoFontTexture,
301
+ };
302
+ return (
303
+ <>
304
+ <block
305
+ args={[
306
+ {
307
+ ...commonArgs,
308
+ fontSize: 0.02,
309
+ },
310
+ ]}
311
+ position={[xCoordinate, -0.1, -0.5]}>
312
+ <ThreeMeshUIText
313
+ content="FAIR Seamless Streaming Demo"
314
+ fontColor={BLACK}
315
+ />
316
+ </block>
317
+ <block
318
+ args={[
319
+ {
320
+ ...commonArgs,
321
+ fontSize: 0.016,
322
+ backgroundOpacity: 0,
323
+ },
324
+ ]}
325
+ position={[xCoordinate, -0.15, -0.5001]}>
326
+ <ThreeMeshUIText
327
+ fontColor={BLACK}
328
+ content="Welcome to the Seamless team streaming demo experience! In this demo, you would experience AI powered text and audio translation in real time."
329
+ />
330
+ </block>
331
+ <block
332
+ args={[
333
+ {
334
+ width: 0.1,
335
+ height: 0.1,
336
+ backgroundOpacity: 1,
337
+ backgroundColor: BLACK,
338
+ },
339
+ ]}
340
+ position={[xCoordinate, -0.23, -0.5002]}>
341
+ <Button
342
+ onClick={() => setStarted(true)}
343
+ content={'Start Experience'}
344
+ width={0.2}
345
+ height={0.035}
346
+ fontSize={0.015}
347
+ padding={0.01}
348
+ borderRadius={0.01}
349
+ />
350
+ </block>
351
+ </>
352
+ );
353
+ }
354
+
355
+ export type XRConfigProps = {
356
+ animateTextDisplay: boolean;
357
+ bufferedSpeechPlayer: BufferedSpeechPlayer;
358
+ translationSentences: TranslationSentences;
359
+ roomState: RoomState | null;
360
+ roomID: string | null;
361
+ startStreaming: () => Promise<void>;
362
+ stopStreaming: () => Promise<void>;
363
+ debugParam: boolean | null;
364
+ };
365
+
366
+ export default function XRConfig(props: XRConfigProps) {
367
+ const {bufferedSpeechPlayer, debugParam} = props;
368
+ const skipARIntro = getURLParams().skipARIntro;
369
+ const defaultDimensions = {width: 500, height: 500};
370
+ const [dimensions, setDimensions] = useState(
371
+ debugParam ? defaultDimensions : {width: 0, height: 0},
372
+ );
373
+ const {width, height} = dimensions;
374
+
375
+ // Make sure to reset buffer when headset is taken off / on so we don't get an endless stream
376
+ // of audio. The oculus actually runs for some time after the headset is taken off.
377
+ const resetBuffers = useCallback(
378
+ (event: XREvent<XRSessionEvent>) => {
379
+ const session = event.target;
380
+ if (!(session instanceof XRSession)) {
381
+ return;
382
+ }
383
+ switch (session.visibilityState) {
384
+ case 'visible':
385
+ bufferedSpeechPlayer.start();
386
+ break;
387
+ case 'hidden':
388
+ bufferedSpeechPlayer.stop();
389
+ break;
390
+ }
391
+ },
392
+ [bufferedSpeechPlayer],
393
+ );
394
+
395
+ return (
396
+ <div style={{height, width, margin: '0 auto', border: '1px solid #ccc'}}>
397
+ {/* This is the button that triggers AR flow if available via a button */}
398
+ <ARButton
399
+ onError={(e) => console.error(e)}
400
+ onClick={() => setDimensions(defaultDimensions)}
401
+ style={{
402
+ position: 'absolute',
403
+ bottom: '24px',
404
+ left: '50%',
405
+ transform: 'translateX(-50%)',
406
+ padding: '12px 24px',
407
+ border: '1px solid white',
408
+ borderRadius: '4px',
409
+ backgroundColor: '#465a69',
410
+ color: 'white',
411
+ font: 'normal 0.8125rem sans-serif',
412
+ outline: 'none',
413
+ zIndex: 99999,
414
+ cursor: 'pointer',
415
+ }}
416
+ />
417
+ {/* Canvas to draw if in browser but if in AR mode displays in pass through mode */}
418
+ {/* The camera here just works in 2D mode. In AR mode it starts at at origin */}
419
+ {/* <Canvas camera={{position: [0, 0, 1], fov: 60}}> */}
420
+ <Canvas camera={{position: [0, 0, 0.001], fov: 60}}>
421
+ <color attach="background" args={['grey']} />
422
+ <XR referenceSpace="local" onVisibilityChange={resetBuffers}>
423
+ {/*
424
+ Uncomment this for controllers to show up
425
+ <Controllers />
426
+ */}
427
+ <Hands />
428
+
429
+ {/*
430
+ Uncomment this for moving with controllers
431
+ <MovementController />
432
+ */}
433
+ {/*
434
+ Uncomment this for turning the view in non-vr mode
435
+ <OrbitControls
436
+ autoRotateSpeed={0.85}
437
+ zoomSpeed={1}
438
+ minPolarAngle={Math.PI / 2.5}
439
+ maxPolarAngle={Math.PI / 2.55}
440
+ />
441
+ */}
442
+ <ThreeMeshUIComponents {...props} skipARIntro={skipARIntro} />
443
+ {/* Just for testing */}
444
+ {/* <RandomComponents /> */}
445
+ </XR>
446
+ </Canvas>
447
+ </div>
448
+ );
449
+ }
streaming-react-app/src/react-xr/XRDialog.css ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .xr-dialog-button {
2
+ margin-bottom: 16px;
3
+ padding-left: 20px;
4
+ padding-right: 20px;
5
+ }
6
+
7
+ .xr-dialog-container {
8
+ min-height: 200px;
9
+ }
10
+
11
+ .xr-dialog-text-center {
12
+ text-align: center;
13
+ }
streaming-react-app/src/react-xr/XRDialog.tsx ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import {
2
+ Button,
3
+ Dialog,
4
+ DialogContent,
5
+ DialogTitle,
6
+ IconButton,
7
+ Typography,
8
+ } from '@mui/material';
9
+ import CloseIcon from '@mui/icons-material/Close';
10
+ import XRConfig, {XRConfigProps} from './XRConfig';
11
+ import {useState} from 'react';
12
+ import './XRDialog.css';
13
+
14
+ export default function XRDialog(props: XRConfigProps) {
15
+ const [isDialogOpen, setIsDialogOpen] = useState<boolean>(false);
16
+ return (
17
+ <>
18
+ <Button variant="contained" onClick={() => setIsDialogOpen(true)}>
19
+ Enter AR Experience
20
+ </Button>
21
+ <Dialog onClose={() => setIsDialogOpen(false)} open={isDialogOpen}>
22
+ <DialogTitle sx={{m: 0, p: 2}} className="xr-dialog-text-center">
23
+ FAIR Seamless Streaming Demo
24
+ </DialogTitle>
25
+ <IconButton
26
+ aria-label="close"
27
+ onClick={() => setIsDialogOpen(false)}
28
+ sx={{
29
+ position: 'absolute',
30
+ right: 8,
31
+ top: 8,
32
+ color: (theme) => theme.palette.grey[500],
33
+ }}>
34
+ <CloseIcon />
35
+ </IconButton>
36
+ <DialogContent
37
+ dividers
38
+ className="xr-dialog-container xr-dialog-text-center">
39
+ <Typography gutterBottom>
40
+ Welcome to the Seamless team streaming demo experience! In this demo
41
+ you will experience AI powered text and audio translation in real
42
+ time.
43
+ </Typography>
44
+ <XRConfig {...props} />
45
+ </DialogContent>
46
+ </Dialog>
47
+ </>
48
+ );
49
+ }
streaming-react-app/src/setURLParam.ts ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export default function setURLParam<T>(
2
+ paramName: string,
3
+ value: T,
4
+ // If there's no defaultValue specified then we always set the URL param explicitly
5
+ defaultValue?: T,
6
+ ): void {
7
+ const urlParams = new URLSearchParams(window.location.search);
8
+ if (defaultValue != null && value === defaultValue) {
9
+ urlParams.delete(paramName);
10
+ } else {
11
+ let stringValue: string;
12
+
13
+ switch (typeof value) {
14
+ case 'string':
15
+ stringValue = value;
16
+ break;
17
+ case 'boolean':
18
+ stringValue = value ? '1' : '0';
19
+ break;
20
+ default:
21
+ throw new Error(`Unsupported URL param type: ${typeof value}`);
22
+ }
23
+
24
+ if (urlParams.has(paramName)) {
25
+ urlParams.set(paramName, stringValue);
26
+ } else {
27
+ urlParams.append(paramName, stringValue);
28
+ }
29
+ }
30
+
31
+ const paramStringWithoutQuestionMark = urlParams.toString();
32
+
33
+ window.history.replaceState(
34
+ null,
35
+ '',
36
+ `${window.location.pathname}${
37
+ paramStringWithoutQuestionMark.length > 0 ? '?' : ''
38
+ }${paramStringWithoutQuestionMark}`,
39
+ );
40
+ }
streaming-react-app/src/sliceTranslationSentencesUtils.ts ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import {TranslationSentences} from './types/StreamingTypes';
2
+
3
+ export function getTotalSentencesLength(
4
+ translatedSentences: TranslationSentences,
5
+ ) {
6
+ return translatedSentences.reduce((acc, curr) => acc + curr.length, 0);
7
+ }
8
+
9
+ /**
10
+ * @returns A new array of strings where the total length of the strings === targetIndex,
11
+ * aka it's as if we joined all the strings together, called joined.slice(0, targetIndex), and then
12
+ * split the string back into an array of strings.
13
+ */
14
+ export function sliceTranslationSentencesUpToIndex(
15
+ translatedSentences: TranslationSentences,
16
+ targetIndex: number,
17
+ ): TranslationSentences {
18
+ return translatedSentences.reduce<TranslationSentences>((acc, sentence) => {
19
+ const accTotalLength = getTotalSentencesLength(acc);
20
+ if (accTotalLength === targetIndex) {
21
+ return acc;
22
+ }
23
+ // If adding the current sentence does not exceed the targetIndex, then add the whole sentence
24
+ if (accTotalLength + sentence.length <= targetIndex) {
25
+ return [...acc, sentence];
26
+ }
27
+ // If adding the current sentence DOES exceed the targetIndex, then slice the sentence and add it
28
+ return [...acc, sentence.slice(0, targetIndex - accTotalLength)];
29
+ }, []);
30
+ }