livekit-agents 0.8.0.dev2__tar.gz → 0.8.0.dev8__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. {livekit_agents-0.8.0.dev2 → livekit_agents-0.8.0.dev8}/PKG-INFO +4 -4
  2. {livekit_agents-0.8.0.dev2 → livekit_agents-0.8.0.dev8}/livekit/agents/cli/watcher.py +1 -1
  3. {livekit_agents-0.8.0.dev2 → livekit_agents-0.8.0.dev8}/livekit/agents/ipc/channel.py +1 -1
  4. {livekit_agents-0.8.0.dev2 → livekit_agents-0.8.0.dev8}/livekit/agents/ipc/proc_main.py +18 -13
  5. {livekit_agents-0.8.0.dev2 → livekit_agents-0.8.0.dev8}/livekit/agents/ipc/proc_pool.py +3 -11
  6. {livekit_agents-0.8.0.dev2 → livekit_agents-0.8.0.dev8}/livekit/agents/ipc/proto.py +0 -1
  7. {livekit_agents-0.8.0.dev2 → livekit_agents-0.8.0.dev8}/livekit/agents/ipc/supervised_proc.py +22 -15
  8. {livekit_agents-0.8.0.dev2 → livekit_agents-0.8.0.dev8}/livekit/agents/job.py +8 -4
  9. {livekit_agents-0.8.0.dev2 → livekit_agents-0.8.0.dev8}/livekit/agents/stt/stt.py +1 -1
  10. {livekit_agents-0.8.0.dev2 → livekit_agents-0.8.0.dev8}/livekit/agents/transcription/_utils.py +2 -2
  11. {livekit_agents-0.8.0.dev2 → livekit_agents-0.8.0.dev8}/livekit/agents/transcription/stt_forwarder.py +8 -3
  12. {livekit_agents-0.8.0.dev2 → livekit_agents-0.8.0.dev8}/livekit/agents/transcription/tts_forwarder.py +34 -17
  13. {livekit_agents-0.8.0.dev2 → livekit_agents-0.8.0.dev8}/livekit/agents/utils/aio/__init__.py +3 -1
  14. {livekit_agents-0.8.0.dev2 → livekit_agents-0.8.0.dev8}/livekit/agents/vad.py +3 -3
  15. {livekit_agents-0.8.0.dev2 → livekit_agents-0.8.0.dev8}/livekit/agents/version.py +1 -1
  16. {livekit_agents-0.8.0.dev2 → livekit_agents-0.8.0.dev8}/livekit/agents/voice_assistant/agent_output.py +50 -52
  17. {livekit_agents-0.8.0.dev2 → livekit_agents-0.8.0.dev8}/livekit/agents/voice_assistant/cancellable_source.py +13 -11
  18. {livekit_agents-0.8.0.dev2 → livekit_agents-0.8.0.dev8}/livekit/agents/voice_assistant/human_input.py +28 -17
  19. {livekit_agents-0.8.0.dev2 → livekit_agents-0.8.0.dev8}/livekit/agents/voice_assistant/voice_assistant.py +89 -49
  20. {livekit_agents-0.8.0.dev2 → livekit_agents-0.8.0.dev8}/livekit/agents/worker.py +34 -14
  21. {livekit_agents-0.8.0.dev2 → livekit_agents-0.8.0.dev8}/livekit_agents.egg-info/PKG-INFO +4 -4
  22. {livekit_agents-0.8.0.dev2 → livekit_agents-0.8.0.dev8}/livekit_agents.egg-info/requires.txt +3 -3
  23. {livekit_agents-0.8.0.dev2 → livekit_agents-0.8.0.dev8}/setup.py +3 -3
  24. {livekit_agents-0.8.0.dev2 → livekit_agents-0.8.0.dev8}/README.md +0 -0
  25. {livekit_agents-0.8.0.dev2 → livekit_agents-0.8.0.dev8}/livekit/agents/__init__.py +0 -0
  26. {livekit_agents-0.8.0.dev2 → livekit_agents-0.8.0.dev8}/livekit/agents/cli/__init__.py +0 -0
  27. {livekit_agents-0.8.0.dev2 → livekit_agents-0.8.0.dev8}/livekit/agents/cli/cli.py +0 -0
  28. {livekit_agents-0.8.0.dev2 → livekit_agents-0.8.0.dev8}/livekit/agents/cli/log.py +0 -0
  29. {livekit_agents-0.8.0.dev2 → livekit_agents-0.8.0.dev8}/livekit/agents/cli/proto.py +0 -0
  30. {livekit_agents-0.8.0.dev2 → livekit_agents-0.8.0.dev8}/livekit/agents/exceptions.py +0 -0
  31. {livekit_agents-0.8.0.dev2 → livekit_agents-0.8.0.dev8}/livekit/agents/http_server.py +0 -0
  32. {livekit_agents-0.8.0.dev2 → livekit_agents-0.8.0.dev8}/livekit/agents/ipc/__init__.py +0 -0
  33. {livekit_agents-0.8.0.dev2 → livekit_agents-0.8.0.dev8}/livekit/agents/llm/__init__.py +0 -0
  34. {livekit_agents-0.8.0.dev2 → livekit_agents-0.8.0.dev8}/livekit/agents/llm/_oai_api.py +0 -0
  35. {livekit_agents-0.8.0.dev2 → livekit_agents-0.8.0.dev8}/livekit/agents/llm/chat_context.py +0 -0
  36. {livekit_agents-0.8.0.dev2 → livekit_agents-0.8.0.dev8}/livekit/agents/llm/function_context.py +0 -0
  37. {livekit_agents-0.8.0.dev2 → livekit_agents-0.8.0.dev8}/livekit/agents/llm/llm.py +0 -0
  38. {livekit_agents-0.8.0.dev2 → livekit_agents-0.8.0.dev8}/livekit/agents/log.py +0 -0
  39. {livekit_agents-0.8.0.dev2 → livekit_agents-0.8.0.dev8}/livekit/agents/plugin.py +0 -0
  40. {livekit_agents-0.8.0.dev2 → livekit_agents-0.8.0.dev8}/livekit/agents/py.typed +0 -0
  41. {livekit_agents-0.8.0.dev2 → livekit_agents-0.8.0.dev8}/livekit/agents/stt/__init__.py +0 -0
  42. {livekit_agents-0.8.0.dev2 → livekit_agents-0.8.0.dev8}/livekit/agents/stt/stream_adapter.py +0 -0
  43. {livekit_agents-0.8.0.dev2 → livekit_agents-0.8.0.dev8}/livekit/agents/tokenize/__init__.py +0 -0
  44. {livekit_agents-0.8.0.dev2 → livekit_agents-0.8.0.dev8}/livekit/agents/tokenize/_basic_hyphenator.py +0 -0
  45. {livekit_agents-0.8.0.dev2 → livekit_agents-0.8.0.dev8}/livekit/agents/tokenize/_basic_paragraph.py +0 -0
  46. {livekit_agents-0.8.0.dev2 → livekit_agents-0.8.0.dev8}/livekit/agents/tokenize/_basic_sent.py +0 -0
  47. {livekit_agents-0.8.0.dev2 → livekit_agents-0.8.0.dev8}/livekit/agents/tokenize/_basic_word.py +0 -0
  48. {livekit_agents-0.8.0.dev2 → livekit_agents-0.8.0.dev8}/livekit/agents/tokenize/basic.py +0 -0
  49. {livekit_agents-0.8.0.dev2 → livekit_agents-0.8.0.dev8}/livekit/agents/tokenize/token_stream.py +0 -0
  50. {livekit_agents-0.8.0.dev2 → livekit_agents-0.8.0.dev8}/livekit/agents/tokenize/tokenizer.py +0 -0
  51. {livekit_agents-0.8.0.dev2 → livekit_agents-0.8.0.dev8}/livekit/agents/transcription/__init__.py +0 -0
  52. {livekit_agents-0.8.0.dev2 → livekit_agents-0.8.0.dev8}/livekit/agents/tts/__init__.py +0 -0
  53. {livekit_agents-0.8.0.dev2 → livekit_agents-0.8.0.dev8}/livekit/agents/tts/stream_adapter.py +0 -0
  54. {livekit_agents-0.8.0.dev2 → livekit_agents-0.8.0.dev8}/livekit/agents/tts/tts.py +0 -0
  55. {livekit_agents-0.8.0.dev2 → livekit_agents-0.8.0.dev8}/livekit/agents/utils/__init__.py +0 -0
  56. {livekit_agents-0.8.0.dev2 → livekit_agents-0.8.0.dev8}/livekit/agents/utils/aio/channel.py +0 -0
  57. {livekit_agents-0.8.0.dev2 → livekit_agents-0.8.0.dev8}/livekit/agents/utils/aio/debug.py +0 -0
  58. {livekit_agents-0.8.0.dev2 → livekit_agents-0.8.0.dev8}/livekit/agents/utils/aio/interval.py +0 -0
  59. {livekit_agents-0.8.0.dev2 → livekit_agents-0.8.0.dev8}/livekit/agents/utils/aio/sleep.py +0 -0
  60. {livekit_agents-0.8.0.dev2 → livekit_agents-0.8.0.dev8}/livekit/agents/utils/aio/task_set.py +0 -0
  61. {livekit_agents-0.8.0.dev2 → livekit_agents-0.8.0.dev8}/livekit/agents/utils/audio.py +0 -0
  62. {livekit_agents-0.8.0.dev2 → livekit_agents-0.8.0.dev8}/livekit/agents/utils/codecs/__init__.py +0 -0
  63. {livekit_agents-0.8.0.dev2 → livekit_agents-0.8.0.dev8}/livekit/agents/utils/codecs/mp3.py +0 -0
  64. {livekit_agents-0.8.0.dev2 → livekit_agents-0.8.0.dev8}/livekit/agents/utils/event_emitter.py +0 -0
  65. {livekit_agents-0.8.0.dev2 → livekit_agents-0.8.0.dev8}/livekit/agents/utils/exp_filter.py +0 -0
  66. {livekit_agents-0.8.0.dev2 → livekit_agents-0.8.0.dev8}/livekit/agents/utils/http_context.py +0 -0
  67. {livekit_agents-0.8.0.dev2 → livekit_agents-0.8.0.dev8}/livekit/agents/utils/images/__init__.py +0 -0
  68. {livekit_agents-0.8.0.dev2 → livekit_agents-0.8.0.dev8}/livekit/agents/utils/images/image.py +0 -0
  69. {livekit_agents-0.8.0.dev2 → livekit_agents-0.8.0.dev8}/livekit/agents/utils/log.py +0 -0
  70. {livekit_agents-0.8.0.dev2 → livekit_agents-0.8.0.dev8}/livekit/agents/utils/misc.py +0 -0
  71. {livekit_agents-0.8.0.dev2 → livekit_agents-0.8.0.dev8}/livekit/agents/utils/moving_average.py +0 -0
  72. {livekit_agents-0.8.0.dev2 → livekit_agents-0.8.0.dev8}/livekit/agents/voice_assistant/__init__.py +0 -0
  73. {livekit_agents-0.8.0.dev2 → livekit_agents-0.8.0.dev8}/livekit/agents/voice_assistant/log.py +0 -0
  74. {livekit_agents-0.8.0.dev2 → livekit_agents-0.8.0.dev8}/livekit/agents/voice_assistant/plotter.py +0 -0
  75. {livekit_agents-0.8.0.dev2 → livekit_agents-0.8.0.dev8}/livekit_agents.egg-info/SOURCES.txt +0 -0
  76. {livekit_agents-0.8.0.dev2 → livekit_agents-0.8.0.dev8}/livekit_agents.egg-info/dependency_links.txt +0 -0
  77. {livekit_agents-0.8.0.dev2 → livekit_agents-0.8.0.dev8}/livekit_agents.egg-info/top_level.txt +0 -0
  78. {livekit_agents-0.8.0.dev2 → livekit_agents-0.8.0.dev8}/pyproject.toml +0 -0
  79. {livekit_agents-0.8.0.dev2 → livekit_agents-0.8.0.dev8}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: livekit-agents
3
- Version: 0.8.0.dev2
3
+ Version: 0.8.0.dev8
4
4
  Summary: LiveKit Python Agents
5
5
  Home-page: https://github.com/livekit/agents
6
6
  License: Apache-2.0
@@ -20,9 +20,9 @@ Classifier: Programming Language :: Python :: 3 :: Only
20
20
  Requires-Python: >=3.9.0
21
21
  Description-Content-Type: text/markdown
22
22
  Requires-Dist: click~=8.1
23
- Requires-Dist: livekit~=0.11
24
- Requires-Dist: livekit-api~=0.4
25
- Requires-Dist: livekit-protocol~=0.4
23
+ Requires-Dist: livekit~=0.12.0.dev1
24
+ Requires-Dist: livekit-api~=0.6.0
25
+ Requires-Dist: livekit-protocol~=0.6.0
26
26
  Requires-Dist: protobuf>=3
27
27
  Requires-Dist: pyjwt>=2.0.0
28
28
  Requires-Dist: types-protobuf<5,>=4
@@ -75,7 +75,7 @@ class WatchServer:
75
75
  self._main_file = main_file
76
76
  self._loop = loop
77
77
 
78
- self._recv_jobs_fut = asyncio.Future()
78
+ self._recv_jobs_fut = asyncio.Future[None]()
79
79
  self._reloading_jobs = False
80
80
 
81
81
  async def run(self) -> None:
@@ -105,7 +105,7 @@ class AsyncProcChannel(ProcChannel):
105
105
 
106
106
  self._read_q = asyncio.Queue[Optional[Message]]()
107
107
  self._write_q = queue.Queue[Optional[Message]]()
108
- self._exit_fut = asyncio.Future()
108
+ self._exit_fut = asyncio.Future[None]()
109
109
 
110
110
  self._read_t = threading.Thread(
111
111
  target=self._read_thread, daemon=True, name="proc_channel_read"
@@ -118,12 +118,15 @@ def _start_job(
118
118
  await room.disconnect()
119
119
 
120
120
  try:
121
- job_shutdown_task = asyncio.create_task(
122
- args.job_shutdown_fnc(job_ctx), name="job_shutdown"
123
- )
124
- await job_shutdown_task
121
+ shutdown_tasks = []
122
+ for callback in job_ctx._shutdown_callbacks:
123
+ shutdown_tasks.append(
124
+ asyncio.create_task(callback(), name="job_shutdown_callback")
125
+ )
126
+
127
+ await asyncio.gather(*shutdown_tasks)
125
128
  except Exception:
126
- logger.exception("error while disconnecting room")
129
+ logger.exception("error while shutting down the job")
127
130
 
128
131
  await utils.http_context._close_http_ctx()
129
132
  exit_proc_fut.set()
@@ -157,9 +160,10 @@ async def _async_main(
157
160
 
158
161
  if isinstance(msg, proto.ShutdownRequest):
159
162
  if job_task is not None:
160
- job_task.shutdown_fut.set_result(
161
- _ShutdownInfo(reason=msg.reason, user_initiated=False)
162
- )
163
+ with contextlib.suppress(asyncio.InvalidStateError):
164
+ job_task.shutdown_fut.set_result(
165
+ _ShutdownInfo(reason=msg.reason, user_initiated=False)
166
+ )
163
167
  else:
164
168
  exit_proc_fut.set() # there is no running job, we can exit immediately
165
169
 
@@ -210,8 +214,9 @@ def main(args: proto.ProcStartArgs) -> None:
210
214
  # (this signal can be sent by watchfiles on dev mode)
211
215
  loop.run_until_complete(main_task)
212
216
  finally:
213
- try:
214
- loop.run_until_complete(loop.shutdown_default_executor())
215
- #loop.run_until_complete(cch.aclose())
216
- finally:
217
- loop.close()
217
+ # try:
218
+ loop.run_until_complete(loop.shutdown_default_executor())
219
+ loop.run_until_complete(cch.aclose())
220
+ # finally:
221
+ # loop.close()
222
+ # pass
@@ -1,8 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import asyncio
4
- import multiprocessing as mp
5
- import sys
4
+ from multiprocessing.context import BaseContext
6
5
  from typing import Any, Callable, Coroutine, Literal
7
6
 
8
7
  from .. import utils
@@ -22,22 +21,16 @@ class ProcPool(utils.EventEmitter[EventTypes]):
22
21
  *,
23
22
  initialize_process_fnc: Callable[[JobProcess], Any],
24
23
  job_entrypoint_fnc: Callable[[JobContext], Coroutine],
25
- job_shutdown_fnc: Callable[[JobContext], Coroutine],
26
24
  num_idle_processes: int,
27
25
  initialize_timeout: float,
28
26
  close_timeout: float,
27
+ mp_ctx: BaseContext,
29
28
  loop: asyncio.AbstractEventLoop,
30
29
  ) -> None:
31
30
  super().__init__()
32
-
33
- #if sys.platform.startswith("linux"):
34
- # self._mp_ctx = mp.get_context("forkserver")
35
- #else:
36
- self._mp_ctx = mp.get_context("spawn")
37
-
31
+ self._mp_ctx = mp_ctx
38
32
  self._initialize_process_fnc = initialize_process_fnc
39
33
  self._job_entrypoint_fnc = job_entrypoint_fnc
40
- self._job_shutdown_fnc = job_shutdown_fnc
41
34
  self._close_timeout = close_timeout
42
35
  self._initialize_timeout = initialize_timeout
43
36
  self._loop = loop
@@ -74,7 +67,6 @@ class ProcPool(utils.EventEmitter[EventTypes]):
74
67
  proc = SupervisedProc(
75
68
  initialize_process_fnc=self._initialize_process_fnc,
76
69
  job_entrypoint_fnc=self._job_entrypoint_fnc,
77
- job_shutdown_fnc=self._job_shutdown_fnc,
78
70
  initialize_timeout=self._initialize_timeout,
79
71
  close_timeout=self._close_timeout,
80
72
  mp_ctx=self._mp_ctx,
@@ -19,7 +19,6 @@ HIGH_PING_THRESHOLD = 0.02 # 20ms
19
19
  class ProcStartArgs:
20
20
  initialize_process_fnc: Callable[[JobProcess], Any]
21
21
  job_entrypoint_fnc: Callable[[JobContext], Coroutine]
22
- job_shutdown_fnc: Callable[[JobContext], Coroutine]
23
22
  log_q: mp.Queue
24
23
  mp_cch: channel.ProcessConn
25
24
  asyncio_debug: bool
@@ -6,7 +6,7 @@ import logging
6
6
  import multiprocessing as mp
7
7
  import sys
8
8
  import threading
9
- from multiprocessing.context import ForkServerContext, SpawnContext
9
+ from multiprocessing.context import BaseContext
10
10
  from typing import Any, Callable, Coroutine
11
11
 
12
12
  from .. import utils
@@ -32,6 +32,8 @@ class LogQueueListener:
32
32
  t.start()
33
33
 
34
34
  def stop(self) -> None:
35
+ if self._thread is None:
36
+ return
35
37
  self._q.put_nowait(self._sentinel)
36
38
  self._thread.join()
37
39
  self._thread = None
@@ -60,14 +62,13 @@ class SupervisedProc:
60
62
  *,
61
63
  initialize_process_fnc: Callable[[JobProcess], Any],
62
64
  job_entrypoint_fnc: Callable[[JobContext], Coroutine],
63
- job_shutdown_fnc: Callable[[JobContext], Coroutine],
64
65
  initialize_timeout: float,
65
66
  close_timeout: float,
66
- mp_ctx: SpawnContext | ForkServerContext,
67
+ mp_ctx: BaseContext,
67
68
  loop: asyncio.AbstractEventLoop,
68
69
  ) -> None:
69
70
  self._loop = loop
70
- log_q = mp.Queue()
71
+ log_q = mp_ctx.Queue()
71
72
  log_q.cancel_join_thread()
72
73
  mp_pch, mp_cch = mp_ctx.Pipe(duplex=True)
73
74
 
@@ -80,13 +81,12 @@ class SupervisedProc:
80
81
  self._proc_args = proto.ProcStartArgs(
81
82
  initialize_process_fnc=initialize_process_fnc,
82
83
  job_entrypoint_fnc=job_entrypoint_fnc,
83
- job_shutdown_fnc=job_shutdown_fnc,
84
84
  log_q=log_q,
85
85
  mp_cch=mp_cch,
86
86
  asyncio_debug=loop.get_debug(),
87
87
  )
88
88
 
89
- self._proc = mp_ctx.Process(
89
+ self._proc = mp_ctx.Process( # type: ignore
90
90
  target=proc_main.main, args=(self._proc_args,), name="job_proc"
91
91
  )
92
92
  self._running_job: RunningJobInfo | None = None
@@ -97,7 +97,7 @@ class SupervisedProc:
97
97
  self._main_atask: asyncio.Task[None] | None = None
98
98
  self._closing = False
99
99
  self._kill_sent = False
100
- self._initialize_fut = asyncio.Future()
100
+ self._initialize_fut = asyncio.Future[None]()
101
101
 
102
102
  @property
103
103
  def exitcode(self) -> int | None:
@@ -145,7 +145,7 @@ class SupervisedProc:
145
145
 
146
146
  self._proc.start()
147
147
  self._pid = self._proc.pid
148
- self._join_fut = asyncio.Future()
148
+ self._join_fut = asyncio.Future[None]()
149
149
 
150
150
  def _sync_run():
151
151
  self._proc.join()
@@ -161,7 +161,8 @@ class SupervisedProc:
161
161
  if not self.started:
162
162
  raise RuntimeError("process not started")
163
163
 
164
- await asyncio.shield(self._main_atask)
164
+ if self._main_atask:
165
+ await asyncio.shield(self._main_atask)
165
166
 
166
167
  async def initialize(self) -> None:
167
168
  """initialize the job process, this is calling the user provided initialize_process_fnc
@@ -198,16 +199,18 @@ class SupervisedProc:
198
199
  await self._pch.asend(proto.ShutdownRequest())
199
200
 
200
201
  try:
201
- await asyncio.wait_for(
202
- asyncio.shield(self._main_atask), timeout=self._close_timeout
203
- )
202
+ if self._main_atask:
203
+ await asyncio.wait_for(
204
+ asyncio.shield(self._main_atask), timeout=self._close_timeout
205
+ )
204
206
  except asyncio.TimeoutError:
205
207
  logger.error(
206
208
  "process did not exit in time, killing job", extra=self.logging_extra()
207
209
  )
208
210
  self._send_kill_signal()
209
211
 
210
- await asyncio.shield(self._main_atask)
212
+ if self._main_atask:
213
+ await asyncio.shield(self._main_atask)
211
214
 
212
215
  async def kill(self) -> None:
213
216
  """forcefully kill the job process"""
@@ -216,7 +219,8 @@ class SupervisedProc:
216
219
 
217
220
  self._closing = True
218
221
  self._send_kill_signal()
219
- await asyncio.shield(self._main_atask)
222
+ if self._main_atask:
223
+ await asyncio.shield(self._main_atask)
220
224
 
221
225
  async def launch_job(self, info: RunningJobInfo) -> None:
222
226
  """start/assign a job to the process"""
@@ -230,7 +234,10 @@ class SupervisedProc:
230
234
 
231
235
  def _send_kill_signal(self) -> None:
232
236
  """forcefully kill the job process"""
233
- if not self._proc.is_alive():
237
+ try:
238
+ if not self._proc.is_alive():
239
+ return
240
+ except ValueError:
234
241
  return
235
242
 
236
243
  logger.debug("killing job process", extra=self.logging_extra())
@@ -61,6 +61,7 @@ class JobContext:
61
61
  self._room = room
62
62
  self._on_connect = on_connect
63
63
  self._on_shutdown = on_shutdown
64
+ self._shutdown_callbacks: list[Callable[[], Coroutine]] = []
64
65
 
65
66
  @property
66
67
  def proc(self) -> JobProcess:
@@ -78,6 +79,9 @@ class JobContext:
78
79
  def agent(self) -> rtc.LocalParticipant:
79
80
  return self._room.local_participant
80
81
 
82
+ def add_shutdown_callback(self, callback: Callable[[], Coroutine]) -> None:
83
+ self._shutdown_callbacks.append(callback)
84
+
81
85
  async def connect(
82
86
  self,
83
87
  *,
@@ -114,8 +118,8 @@ def _apply_auto_subscribe_opts(room: rtc.Room, auto_subscribe: AutoSubscribe) ->
114
118
  ):
115
119
  pub.set_subscribed(True)
116
120
 
117
- for p in room.participants.values():
118
- for pub in p.tracks.values():
121
+ for p in room.remote_participants.values():
122
+ for pub in p.track_publications.values():
119
123
  _subscribe_if_needed(pub)
120
124
 
121
125
  @room.on("track_published")
@@ -128,11 +132,11 @@ def _apply_auto_subscribe_opts(room: rtc.Room, auto_subscribe: AutoSubscribe) ->
128
132
  class JobProcess:
129
133
  def __init__(self, *, start_arguments: Any | None = None) -> None:
130
134
  self._mp_proc = mp.current_process()
131
- self._userdata = {}
135
+ self._userdata: dict[str, Any] = {}
132
136
  self._start_arguments = start_arguments
133
137
 
134
138
  @property
135
- def pid(self) -> int:
139
+ def pid(self) -> int | None:
136
140
  return self._mp_proc.pid
137
141
 
138
142
  @property
@@ -12,7 +12,7 @@ from ..utils import AudioBuffer, aio
12
12
 
13
13
 
14
14
  @unique
15
- class SpeechEventType(Enum):
15
+ class SpeechEventType(str, Enum):
16
16
  START_OF_SPEECH = "start_of_speech"
17
17
  """indicate the start of speech
18
18
  if the STT doesn't support this event, this will be emitted as the same time as the first INTERIM_TRANSCRIPT"""
@@ -7,7 +7,7 @@ from livekit import rtc
7
7
 
8
8
  def find_micro_track_id(room: rtc.Room, identity: str) -> str:
9
9
  p: rtc.RemoteParticipant | rtc.LocalParticipant | None = (
10
- room.participants_by_identity.get(identity)
10
+ room.remote_participants.get(identity)
11
11
  )
12
12
  if identity == room.local_participant.identity:
13
13
  p = room.local_participant
@@ -17,7 +17,7 @@ def find_micro_track_id(room: rtc.Room, identity: str) -> str:
17
17
 
18
18
  # find first micro track
19
19
  track_id = None
20
- for track in p.tracks.values():
20
+ for track in p.track_publications.values():
21
21
  if track.source == rtc.TrackSource.SOURCE_MICROPHONE:
22
22
  track_id = track.sid
23
23
  break
@@ -45,9 +45,8 @@ class STTSegmentsForwarder:
45
45
 
46
46
  transcription = rtc.Transcription(
47
47
  participant_identity=self._participant_identity,
48
- track_id=self._track_id,
48
+ track_sid=self._track_id,
49
49
  segments=[seg], # no history for now
50
- language="", # TODO(theomonnom)
51
50
  )
52
51
  await self._room.local_participant.publish_transcription(transcription)
53
52
 
@@ -66,13 +65,19 @@ class STTSegmentsForwarder:
66
65
  start_time=0,
67
66
  end_time=0,
68
67
  final=False,
68
+ language="", # TODO
69
69
  )
70
70
  )
71
71
  elif ev.type == stt.SpeechEventType.FINAL_TRANSCRIPT:
72
72
  text = ev.alternatives[0].text
73
73
  self._queue.put_nowait(
74
74
  rtc.TranscriptionSegment(
75
- id=self._current_id, text=text, start_time=0, end_time=0, final=True
75
+ id=self._current_id,
76
+ text=text,
77
+ start_time=0,
78
+ end_time=0,
79
+ final=True,
80
+ language="", # TODO
76
81
  )
77
82
  )
78
83
 
@@ -13,6 +13,9 @@ from .. import tokenize, utils
13
13
  from ..log import logger
14
14
  from . import _utils
15
15
 
16
+ # 3.83 is the "baseline", the number of hyphens per second TTS returns in avg.
17
+ STANDARD_SPEECH_RATE = 3.83
18
+
16
19
 
17
20
  @dataclass
18
21
  class _TTSOptions:
@@ -35,7 +38,7 @@ class _SegmentData:
35
38
  pushed_duration: float = 0.0
36
39
  real_speed: float | None = None
37
40
  processed_sentences: int = 0
38
- processed_hyphenes: int = 0
41
+ processed_hyphens: int = 0
39
42
  validated: bool = False
40
43
  forward_start_time: float | None = 0.0
41
44
 
@@ -62,7 +65,7 @@ class TTSSegmentsForwarder:
62
65
  participant: rtc.Participant | str,
63
66
  track: rtc.Track | rtc.TrackPublication | str | None = None,
64
67
  language: str = "",
65
- speed: float = 3.83,
68
+ speed: float = 1.0,
66
69
  new_sentence_delay: float = 0.4,
67
70
  word_tokenizer: tokenize.WordTokenizer = tokenize.basic.WordTokenizer(),
68
71
  sentence_tokenizer: tokenize.SentenceTokenizer = tokenize.basic.SentenceTokenizer(),
@@ -82,7 +85,7 @@ class TTSSegmentsForwarder:
82
85
  to start the transcription.
83
86
  word_tokenizer: word tokenizer used to split the text into words
84
87
  sentence_tokenizer: sentence tokenizer used to split the text into sentences
85
- hyphenate_word: function that returns a list of hyphenes for a given word
88
+ hyphenate_word: function that returns a list of hyphens for a given word
86
89
 
87
90
  """
88
91
  identity = participant if isinstance(participant, str) else participant.identity
@@ -92,6 +95,7 @@ class TTSSegmentsForwarder:
92
95
  elif isinstance(track, (rtc.TrackPublication, rtc.Track)):
93
96
  track = track.sid
94
97
 
98
+ speed = speed * STANDARD_SPEECH_RATE
95
99
  self._opts = _TTSOptions(
96
100
  room=room,
97
101
  participant_identity=identity,
@@ -185,6 +189,10 @@ class TTSSegmentsForwarder:
185
189
  self._forming_segments.q.append(new_seg)
186
190
  self._seg_queue.put_nowait(new_seg)
187
191
 
192
+ @property
193
+ def closed(self) -> bool:
194
+ return self._closed
195
+
188
196
  async def aclose(self) -> None:
189
197
  if self._closed:
190
198
  return
@@ -213,9 +221,8 @@ class TTSSegmentsForwarder:
213
221
 
214
222
  tr = rtc.Transcription(
215
223
  participant_identity=self._opts.participant_identity,
216
- track_id=self._opts.track_id,
224
+ track_sid=self._opts.track_id,
217
225
  segments=[seg], # no history for now, only one segment
218
- language=self._opts.language,
219
226
  )
220
227
  await self._opts.room.local_participant.publish_transcription(tr)
221
228
 
@@ -267,7 +274,7 @@ class TTSSegmentsForwarder:
267
274
  # transcription closed, early
268
275
  return
269
276
 
270
- word_hyphenes = len(self._opts.hyphenate_word(word))
277
+ word_hyphens = len(self._opts.hyphenate_word(word))
271
278
  processed_words.append(word)
272
279
 
273
280
  # elapsed time since the start of the seg
@@ -283,26 +290,36 @@ class TTSSegmentsForwarder:
283
290
  )
284
291
  hyph_pauses = estimated_pauses_s * speed
285
292
 
286
- target_hyphenes = round(speed * elapsed_time)
287
- dt = target_hyphenes - seg.processed_hyphenes - hyph_pauses
288
- to_wait_hyphenes = max(0, word_hyphenes - dt)
289
- delay = to_wait_hyphenes / speed
293
+ target_hyphens = round(speed * elapsed_time)
294
+ dt = target_hyphens - seg.processed_hyphens - hyph_pauses
295
+ to_wait_hyphens = max(0, word_hyphens - dt)
296
+ delay = to_wait_hyphens / speed
290
297
  else:
291
- delay = word_hyphenes / speed
298
+ delay = word_hyphens / speed
292
299
 
293
300
  first_delay = min(delay / 2, 2 / speed)
294
301
  await self._sleep_if_not_closed(first_delay)
295
302
  rtc_seg_q.put_nowait(
296
303
  rtc.TranscriptionSegment(
297
- id=seg_id, text=text, start_time=0, end_time=0, final=False
304
+ id=seg_id,
305
+ text=text,
306
+ start_time=0,
307
+ end_time=0,
308
+ final=False,
309
+ language=self._opts.language,
298
310
  )
299
311
  )
300
312
  await self._sleep_if_not_closed(delay - first_delay)
301
- seg.processed_hyphenes += word_hyphenes
313
+ seg.processed_hyphens += word_hyphens
302
314
 
303
315
  rtc_seg_q.put_nowait(
304
316
  rtc.TranscriptionSegment(
305
- id=seg_id, text=tokenized_sentence, start_time=0, end_time=0, final=True
317
+ id=seg_id,
318
+ text=tokenized_sentence,
319
+ start_time=0,
320
+ end_time=0,
321
+ final=True,
322
+ language=self._opts.language,
306
323
  )
307
324
  )
308
325
 
@@ -314,13 +331,13 @@ class TTSSegmentsForwarder:
314
331
  await asyncio.wait([self._close_future], timeout=delay)
315
332
 
316
333
  def _calc_hyphens(self, text: str) -> list[str]:
317
- hyphenes: list[str] = []
334
+ hyphens: list[str] = []
318
335
  words = self._opts.word_tokenizer.tokenize(text=text)
319
336
  for word in words:
320
337
  new = self._opts.hyphenate_word(word)
321
- hyphenes.extend(new)
338
+ hyphens.extend(new)
322
339
 
323
- return hyphenes
340
+ return hyphens
324
341
 
325
342
  def _create_segment(self) -> _SegmentData:
326
343
  data = _SegmentData(
@@ -1,4 +1,5 @@
1
1
  import asyncio
2
+ import contextlib
2
3
 
3
4
  from . import debug
4
5
  from .channel import Chan, ChanClosed, ChanReceiver, ChanSender
@@ -11,7 +12,8 @@ async def gracefully_cancel(*futures: asyncio.Future):
11
12
  for f in futures:
12
13
  f.cancel()
13
14
 
14
- await asyncio.gather(*futures, return_exceptions=True)
15
+ with contextlib.suppress(asyncio.CancelledError):
16
+ await asyncio.gather(*futures)
15
17
 
16
18
 
17
19
  __all__ = [
@@ -10,7 +10,7 @@ from .utils import aio
10
10
 
11
11
 
12
12
  @unique
13
- class VADEventType(Enum):
13
+ class VADEventType(str, Enum):
14
14
  START_OF_SPEECH = "start_of_speech"
15
15
  INFERENCE_DONE = "inference_done"
16
16
  END_OF_SPEECH = "end_of_speech"
@@ -42,8 +42,8 @@ class VADCapabilities:
42
42
 
43
43
 
44
44
  class VAD(ABC):
45
- def __init__(self, *, capatiilities: VADCapabilities) -> None:
46
- self._capabilities = capatiilities
45
+ def __init__(self, *, capabilities: VADCapabilities) -> None:
46
+ self._capabilities = capabilities
47
47
 
48
48
  @property
49
49
  def capabilities(self) -> VADCapabilities:
@@ -12,4 +12,4 @@
12
12
  # See the License for the specific language governing permissions and
13
13
  # limitations under the License.
14
14
 
15
- __version__ = "0.8.0-dev.2"
15
+ __version__ = "0.8.0-dev.8"