livekit-agents 0.8.0.dev2__tar.gz → 0.8.0.dev3__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.dev3}/PKG-INFO +4 -4
  2. {livekit_agents-0.8.0.dev2 → livekit_agents-0.8.0.dev3}/livekit/agents/cli/watcher.py +1 -1
  3. {livekit_agents-0.8.0.dev2 → livekit_agents-0.8.0.dev3}/livekit/agents/ipc/channel.py +1 -1
  4. {livekit_agents-0.8.0.dev2 → livekit_agents-0.8.0.dev3}/livekit/agents/ipc/proc_main.py +10 -8
  5. {livekit_agents-0.8.0.dev2 → livekit_agents-0.8.0.dev3}/livekit/agents/ipc/proc_pool.py +3 -8
  6. {livekit_agents-0.8.0.dev2 → livekit_agents-0.8.0.dev3}/livekit/agents/ipc/supervised_proc.py +19 -10
  7. {livekit_agents-0.8.0.dev2 → livekit_agents-0.8.0.dev3}/livekit/agents/job.py +4 -4
  8. {livekit_agents-0.8.0.dev2 → livekit_agents-0.8.0.dev3}/livekit/agents/transcription/_utils.py +2 -2
  9. {livekit_agents-0.8.0.dev2 → livekit_agents-0.8.0.dev3}/livekit/agents/transcription/stt_forwarder.py +8 -3
  10. {livekit_agents-0.8.0.dev2 → livekit_agents-0.8.0.dev3}/livekit/agents/transcription/tts_forwarder.py +17 -4
  11. {livekit_agents-0.8.0.dev2 → livekit_agents-0.8.0.dev3}/livekit/agents/version.py +1 -1
  12. {livekit_agents-0.8.0.dev2 → livekit_agents-0.8.0.dev3}/livekit/agents/voice_assistant/agent_output.py +26 -45
  13. {livekit_agents-0.8.0.dev2 → livekit_agents-0.8.0.dev3}/livekit/agents/voice_assistant/cancellable_source.py +13 -11
  14. {livekit_agents-0.8.0.dev2 → livekit_agents-0.8.0.dev3}/livekit/agents/voice_assistant/human_input.py +1 -1
  15. {livekit_agents-0.8.0.dev2 → livekit_agents-0.8.0.dev3}/livekit/agents/voice_assistant/voice_assistant.py +13 -8
  16. {livekit_agents-0.8.0.dev2 → livekit_agents-0.8.0.dev3}/livekit/agents/worker.py +10 -2
  17. {livekit_agents-0.8.0.dev2 → livekit_agents-0.8.0.dev3}/livekit_agents.egg-info/PKG-INFO +4 -4
  18. {livekit_agents-0.8.0.dev2 → livekit_agents-0.8.0.dev3}/livekit_agents.egg-info/requires.txt +3 -3
  19. {livekit_agents-0.8.0.dev2 → livekit_agents-0.8.0.dev3}/setup.py +3 -3
  20. {livekit_agents-0.8.0.dev2 → livekit_agents-0.8.0.dev3}/README.md +0 -0
  21. {livekit_agents-0.8.0.dev2 → livekit_agents-0.8.0.dev3}/livekit/agents/__init__.py +0 -0
  22. {livekit_agents-0.8.0.dev2 → livekit_agents-0.8.0.dev3}/livekit/agents/cli/__init__.py +0 -0
  23. {livekit_agents-0.8.0.dev2 → livekit_agents-0.8.0.dev3}/livekit/agents/cli/cli.py +0 -0
  24. {livekit_agents-0.8.0.dev2 → livekit_agents-0.8.0.dev3}/livekit/agents/cli/log.py +0 -0
  25. {livekit_agents-0.8.0.dev2 → livekit_agents-0.8.0.dev3}/livekit/agents/cli/proto.py +0 -0
  26. {livekit_agents-0.8.0.dev2 → livekit_agents-0.8.0.dev3}/livekit/agents/exceptions.py +0 -0
  27. {livekit_agents-0.8.0.dev2 → livekit_agents-0.8.0.dev3}/livekit/agents/http_server.py +0 -0
  28. {livekit_agents-0.8.0.dev2 → livekit_agents-0.8.0.dev3}/livekit/agents/ipc/__init__.py +0 -0
  29. {livekit_agents-0.8.0.dev2 → livekit_agents-0.8.0.dev3}/livekit/agents/ipc/proto.py +0 -0
  30. {livekit_agents-0.8.0.dev2 → livekit_agents-0.8.0.dev3}/livekit/agents/llm/__init__.py +0 -0
  31. {livekit_agents-0.8.0.dev2 → livekit_agents-0.8.0.dev3}/livekit/agents/llm/_oai_api.py +0 -0
  32. {livekit_agents-0.8.0.dev2 → livekit_agents-0.8.0.dev3}/livekit/agents/llm/chat_context.py +0 -0
  33. {livekit_agents-0.8.0.dev2 → livekit_agents-0.8.0.dev3}/livekit/agents/llm/function_context.py +0 -0
  34. {livekit_agents-0.8.0.dev2 → livekit_agents-0.8.0.dev3}/livekit/agents/llm/llm.py +0 -0
  35. {livekit_agents-0.8.0.dev2 → livekit_agents-0.8.0.dev3}/livekit/agents/log.py +0 -0
  36. {livekit_agents-0.8.0.dev2 → livekit_agents-0.8.0.dev3}/livekit/agents/plugin.py +0 -0
  37. {livekit_agents-0.8.0.dev2 → livekit_agents-0.8.0.dev3}/livekit/agents/py.typed +0 -0
  38. {livekit_agents-0.8.0.dev2 → livekit_agents-0.8.0.dev3}/livekit/agents/stt/__init__.py +0 -0
  39. {livekit_agents-0.8.0.dev2 → livekit_agents-0.8.0.dev3}/livekit/agents/stt/stream_adapter.py +0 -0
  40. {livekit_agents-0.8.0.dev2 → livekit_agents-0.8.0.dev3}/livekit/agents/stt/stt.py +0 -0
  41. {livekit_agents-0.8.0.dev2 → livekit_agents-0.8.0.dev3}/livekit/agents/tokenize/__init__.py +0 -0
  42. {livekit_agents-0.8.0.dev2 → livekit_agents-0.8.0.dev3}/livekit/agents/tokenize/_basic_hyphenator.py +0 -0
  43. {livekit_agents-0.8.0.dev2 → livekit_agents-0.8.0.dev3}/livekit/agents/tokenize/_basic_paragraph.py +0 -0
  44. {livekit_agents-0.8.0.dev2 → livekit_agents-0.8.0.dev3}/livekit/agents/tokenize/_basic_sent.py +0 -0
  45. {livekit_agents-0.8.0.dev2 → livekit_agents-0.8.0.dev3}/livekit/agents/tokenize/_basic_word.py +0 -0
  46. {livekit_agents-0.8.0.dev2 → livekit_agents-0.8.0.dev3}/livekit/agents/tokenize/basic.py +0 -0
  47. {livekit_agents-0.8.0.dev2 → livekit_agents-0.8.0.dev3}/livekit/agents/tokenize/token_stream.py +0 -0
  48. {livekit_agents-0.8.0.dev2 → livekit_agents-0.8.0.dev3}/livekit/agents/tokenize/tokenizer.py +0 -0
  49. {livekit_agents-0.8.0.dev2 → livekit_agents-0.8.0.dev3}/livekit/agents/transcription/__init__.py +0 -0
  50. {livekit_agents-0.8.0.dev2 → livekit_agents-0.8.0.dev3}/livekit/agents/tts/__init__.py +0 -0
  51. {livekit_agents-0.8.0.dev2 → livekit_agents-0.8.0.dev3}/livekit/agents/tts/stream_adapter.py +0 -0
  52. {livekit_agents-0.8.0.dev2 → livekit_agents-0.8.0.dev3}/livekit/agents/tts/tts.py +0 -0
  53. {livekit_agents-0.8.0.dev2 → livekit_agents-0.8.0.dev3}/livekit/agents/utils/__init__.py +0 -0
  54. {livekit_agents-0.8.0.dev2 → livekit_agents-0.8.0.dev3}/livekit/agents/utils/aio/__init__.py +0 -0
  55. {livekit_agents-0.8.0.dev2 → livekit_agents-0.8.0.dev3}/livekit/agents/utils/aio/channel.py +0 -0
  56. {livekit_agents-0.8.0.dev2 → livekit_agents-0.8.0.dev3}/livekit/agents/utils/aio/debug.py +0 -0
  57. {livekit_agents-0.8.0.dev2 → livekit_agents-0.8.0.dev3}/livekit/agents/utils/aio/interval.py +0 -0
  58. {livekit_agents-0.8.0.dev2 → livekit_agents-0.8.0.dev3}/livekit/agents/utils/aio/sleep.py +0 -0
  59. {livekit_agents-0.8.0.dev2 → livekit_agents-0.8.0.dev3}/livekit/agents/utils/aio/task_set.py +0 -0
  60. {livekit_agents-0.8.0.dev2 → livekit_agents-0.8.0.dev3}/livekit/agents/utils/audio.py +0 -0
  61. {livekit_agents-0.8.0.dev2 → livekit_agents-0.8.0.dev3}/livekit/agents/utils/codecs/__init__.py +0 -0
  62. {livekit_agents-0.8.0.dev2 → livekit_agents-0.8.0.dev3}/livekit/agents/utils/codecs/mp3.py +0 -0
  63. {livekit_agents-0.8.0.dev2 → livekit_agents-0.8.0.dev3}/livekit/agents/utils/event_emitter.py +0 -0
  64. {livekit_agents-0.8.0.dev2 → livekit_agents-0.8.0.dev3}/livekit/agents/utils/exp_filter.py +0 -0
  65. {livekit_agents-0.8.0.dev2 → livekit_agents-0.8.0.dev3}/livekit/agents/utils/http_context.py +0 -0
  66. {livekit_agents-0.8.0.dev2 → livekit_agents-0.8.0.dev3}/livekit/agents/utils/images/__init__.py +0 -0
  67. {livekit_agents-0.8.0.dev2 → livekit_agents-0.8.0.dev3}/livekit/agents/utils/images/image.py +0 -0
  68. {livekit_agents-0.8.0.dev2 → livekit_agents-0.8.0.dev3}/livekit/agents/utils/log.py +0 -0
  69. {livekit_agents-0.8.0.dev2 → livekit_agents-0.8.0.dev3}/livekit/agents/utils/misc.py +0 -0
  70. {livekit_agents-0.8.0.dev2 → livekit_agents-0.8.0.dev3}/livekit/agents/utils/moving_average.py +0 -0
  71. {livekit_agents-0.8.0.dev2 → livekit_agents-0.8.0.dev3}/livekit/agents/vad.py +0 -0
  72. {livekit_agents-0.8.0.dev2 → livekit_agents-0.8.0.dev3}/livekit/agents/voice_assistant/__init__.py +0 -0
  73. {livekit_agents-0.8.0.dev2 → livekit_agents-0.8.0.dev3}/livekit/agents/voice_assistant/log.py +0 -0
  74. {livekit_agents-0.8.0.dev2 → livekit_agents-0.8.0.dev3}/livekit/agents/voice_assistant/plotter.py +0 -0
  75. {livekit_agents-0.8.0.dev2 → livekit_agents-0.8.0.dev3}/livekit_agents.egg-info/SOURCES.txt +0 -0
  76. {livekit_agents-0.8.0.dev2 → livekit_agents-0.8.0.dev3}/livekit_agents.egg-info/dependency_links.txt +0 -0
  77. {livekit_agents-0.8.0.dev2 → livekit_agents-0.8.0.dev3}/livekit_agents.egg-info/top_level.txt +0 -0
  78. {livekit_agents-0.8.0.dev2 → livekit_agents-0.8.0.dev3}/pyproject.toml +0 -0
  79. {livekit_agents-0.8.0.dev2 → livekit_agents-0.8.0.dev3}/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.dev3
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"
@@ -157,9 +157,10 @@ async def _async_main(
157
157
 
158
158
  if isinstance(msg, proto.ShutdownRequest):
159
159
  if job_task is not None:
160
- job_task.shutdown_fut.set_result(
161
- _ShutdownInfo(reason=msg.reason, user_initiated=False)
162
- )
160
+ with contextlib.suppress(asyncio.InvalidStateError):
161
+ job_task.shutdown_fut.set_result(
162
+ _ShutdownInfo(reason=msg.reason, user_initiated=False)
163
+ )
163
164
  else:
164
165
  exit_proc_fut.set() # there is no running job, we can exit immediately
165
166
 
@@ -210,8 +211,9 @@ def main(args: proto.ProcStartArgs) -> None:
210
211
  # (this signal can be sent by watchfiles on dev mode)
211
212
  loop.run_until_complete(main_task)
212
213
  finally:
213
- try:
214
- loop.run_until_complete(loop.shutdown_default_executor())
215
- #loop.run_until_complete(cch.aclose())
216
- finally:
217
- loop.close()
214
+ # try:
215
+ loop.run_until_complete(loop.shutdown_default_executor())
216
+ loop.run_until_complete(cch.aclose())
217
+ # finally:
218
+ # loop.close()
219
+ # 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 ForkServerContext, SpawnContext
6
5
  from typing import Any, Callable, Coroutine, Literal
7
6
 
8
7
  from .. import utils
@@ -26,15 +25,11 @@ class ProcPool(utils.EventEmitter[EventTypes]):
26
25
  num_idle_processes: int,
27
26
  initialize_timeout: float,
28
27
  close_timeout: float,
28
+ mp_ctx: ForkServerContext | SpawnContext,
29
29
  loop: asyncio.AbstractEventLoop,
30
30
  ) -> None:
31
31
  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
-
32
+ self._mp_ctx = mp_ctx
38
33
  self._initialize_process_fnc = initialize_process_fnc
39
34
  self._job_entrypoint_fnc = job_entrypoint_fnc
40
35
  self._job_shutdown_fnc = job_shutdown_fnc
@@ -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
@@ -67,7 +69,7 @@ class SupervisedProc:
67
69
  loop: asyncio.AbstractEventLoop,
68
70
  ) -> None:
69
71
  self._loop = loop
70
- log_q = mp.Queue()
72
+ log_q = mp_ctx.Queue()
71
73
  log_q.cancel_join_thread()
72
74
  mp_pch, mp_cch = mp_ctx.Pipe(duplex=True)
73
75
 
@@ -97,7 +99,7 @@ class SupervisedProc:
97
99
  self._main_atask: asyncio.Task[None] | None = None
98
100
  self._closing = False
99
101
  self._kill_sent = False
100
- self._initialize_fut = asyncio.Future()
102
+ self._initialize_fut = asyncio.Future[None]()
101
103
 
102
104
  @property
103
105
  def exitcode(self) -> int | None:
@@ -145,7 +147,7 @@ class SupervisedProc:
145
147
 
146
148
  self._proc.start()
147
149
  self._pid = self._proc.pid
148
- self._join_fut = asyncio.Future()
150
+ self._join_fut = asyncio.Future[None]()
149
151
 
150
152
  def _sync_run():
151
153
  self._proc.join()
@@ -161,7 +163,8 @@ class SupervisedProc:
161
163
  if not self.started:
162
164
  raise RuntimeError("process not started")
163
165
 
164
- await asyncio.shield(self._main_atask)
166
+ if self._main_atask:
167
+ await asyncio.shield(self._main_atask)
165
168
 
166
169
  async def initialize(self) -> None:
167
170
  """initialize the job process, this is calling the user provided initialize_process_fnc
@@ -198,16 +201,18 @@ class SupervisedProc:
198
201
  await self._pch.asend(proto.ShutdownRequest())
199
202
 
200
203
  try:
201
- await asyncio.wait_for(
202
- asyncio.shield(self._main_atask), timeout=self._close_timeout
203
- )
204
+ if self._main_atask:
205
+ await asyncio.wait_for(
206
+ asyncio.shield(self._main_atask), timeout=self._close_timeout
207
+ )
204
208
  except asyncio.TimeoutError:
205
209
  logger.error(
206
210
  "process did not exit in time, killing job", extra=self.logging_extra()
207
211
  )
208
212
  self._send_kill_signal()
209
213
 
210
- await asyncio.shield(self._main_atask)
214
+ if self._main_atask:
215
+ await asyncio.shield(self._main_atask)
211
216
 
212
217
  async def kill(self) -> None:
213
218
  """forcefully kill the job process"""
@@ -216,7 +221,8 @@ class SupervisedProc:
216
221
 
217
222
  self._closing = True
218
223
  self._send_kill_signal()
219
- await asyncio.shield(self._main_atask)
224
+ if self._main_atask:
225
+ await asyncio.shield(self._main_atask)
220
226
 
221
227
  async def launch_job(self, info: RunningJobInfo) -> None:
222
228
  """start/assign a job to the process"""
@@ -230,7 +236,10 @@ class SupervisedProc:
230
236
 
231
237
  def _send_kill_signal(self) -> None:
232
238
  """forcefully kill the job process"""
233
- if not self._proc.is_alive():
239
+ try:
240
+ if not self._proc.is_alive():
241
+ return
242
+ except ValueError:
234
243
  return
235
244
 
236
245
  logger.debug("killing job process", extra=self.logging_extra())
@@ -114,8 +114,8 @@ def _apply_auto_subscribe_opts(room: rtc.Room, auto_subscribe: AutoSubscribe) ->
114
114
  ):
115
115
  pub.set_subscribed(True)
116
116
 
117
- for p in room.participants.values():
118
- for pub in p.tracks.values():
117
+ for p in room.remote_participants.values():
118
+ for pub in p.track_publications.values():
119
119
  _subscribe_if_needed(pub)
120
120
 
121
121
  @room.on("track_published")
@@ -128,11 +128,11 @@ def _apply_auto_subscribe_opts(room: rtc.Room, auto_subscribe: AutoSubscribe) ->
128
128
  class JobProcess:
129
129
  def __init__(self, *, start_arguments: Any | None = None) -> None:
130
130
  self._mp_proc = mp.current_process()
131
- self._userdata = {}
131
+ self._userdata: dict[str, Any] = {}
132
132
  self._start_arguments = start_arguments
133
133
 
134
134
  @property
135
- def pid(self) -> int:
135
+ def pid(self) -> int | None:
136
136
  return self._mp_proc.pid
137
137
 
138
138
  @property
@@ -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
 
@@ -185,6 +185,10 @@ class TTSSegmentsForwarder:
185
185
  self._forming_segments.q.append(new_seg)
186
186
  self._seg_queue.put_nowait(new_seg)
187
187
 
188
+ @property
189
+ def closed(self) -> bool:
190
+ return self._closed
191
+
188
192
  async def aclose(self) -> None:
189
193
  if self._closed:
190
194
  return
@@ -213,9 +217,8 @@ class TTSSegmentsForwarder:
213
217
 
214
218
  tr = rtc.Transcription(
215
219
  participant_identity=self._opts.participant_identity,
216
- track_id=self._opts.track_id,
220
+ track_sid=self._opts.track_id,
217
221
  segments=[seg], # no history for now, only one segment
218
- language=self._opts.language,
219
222
  )
220
223
  await self._opts.room.local_participant.publish_transcription(tr)
221
224
 
@@ -294,7 +297,12 @@ class TTSSegmentsForwarder:
294
297
  await self._sleep_if_not_closed(first_delay)
295
298
  rtc_seg_q.put_nowait(
296
299
  rtc.TranscriptionSegment(
297
- id=seg_id, text=text, start_time=0, end_time=0, final=False
300
+ id=seg_id,
301
+ text=text,
302
+ start_time=0,
303
+ end_time=0,
304
+ final=False,
305
+ language=self._opts.language,
298
306
  )
299
307
  )
300
308
  await self._sleep_if_not_closed(delay - first_delay)
@@ -302,7 +310,12 @@ class TTSSegmentsForwarder:
302
310
 
303
311
  rtc_seg_q.put_nowait(
304
312
  rtc.TranscriptionSegment(
305
- id=seg_id, text=tokenized_sentence, start_time=0, end_time=0, final=True
313
+ id=seg_id,
314
+ text=tokenized_sentence,
315
+ start_time=0,
316
+ end_time=0,
317
+ final=True,
318
+ language=self._opts.language,
306
319
  )
307
320
  )
308
321
 
@@ -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.3"
@@ -1,7 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import asyncio
4
- import contextlib
4
+ import time
5
5
  from typing import Any, AsyncIterable, Union
6
6
 
7
7
  from livekit import rtc
@@ -116,53 +116,42 @@ class AgentOutput:
116
116
  co = _stream_synthesis_task(handle._speech_source, handle)
117
117
 
118
118
  synth = asyncio.create_task(co)
119
+ synth.add_done_callback(lambda _: handle._buf_ch.close())
119
120
  try:
120
121
  _ = await asyncio.wait(
121
122
  [synth, handle._interrupt_fut], return_when=asyncio.FIRST_COMPLETED
122
123
  )
123
124
  finally:
124
- with contextlib.suppress(asyncio.CancelledError):
125
- synth.cancel()
126
- await synth
127
-
128
- try:
129
- if handle.play_handle is not None:
130
- await handle.play_handle
131
- finally:
132
- await handle._tr_fwd.aclose()
125
+ await utils.aio.gracefully_cancel(synth)
133
126
 
134
127
 
135
128
  @utils.log_exceptions(logger=logger)
136
129
  async def _str_synthesis_task(text: str, handle: SynthesisHandle) -> None:
137
130
  """synthesize speech from a string"""
138
- if handle._tr_fwd is not None:
131
+ if handle._tr_fwd and not handle._tr_fwd.closed:
139
132
  handle._tr_fwd.push_text(text)
140
133
  handle._tr_fwd.mark_text_segment_end()
141
134
 
142
- # start_time = time.time()
143
- # first_frame = True
144
- # audio_duration = 0.0
135
+ start_time = time.time()
136
+ first_frame = True
145
137
  handle._collected_text = text
146
138
 
147
139
  try:
148
140
  async for audio in handle._tts.synthesize(text):
149
- # if first_frame:
150
- # first_frame = False
151
- # dt = time.time() - start_time
152
- # self._log_debug(f"tts first frame in {dt:.2f}s")
141
+ if first_frame:
142
+ first_frame = False
143
+ dt = time.time() - start_time
144
+ logger.debug(f"AgentOutput._str_synthesis_task: TTFB in {dt:.2f}s")
153
145
 
154
146
  frame = audio.frame
155
- # audio_duration += frame.samples_per_channel / frame.sample_rate
156
147
 
157
148
  handle._buf_ch.send_nowait(frame)
158
- if handle._tr_fwd is not None:
149
+ if handle._tr_fwd and not handle._tr_fwd.closed:
159
150
  handle._tr_fwd.push_audio(frame)
160
151
 
161
152
  finally:
162
- if handle._tr_fwd is not None:
153
+ if handle._tr_fwd and not handle._tr_fwd.closed:
163
154
  handle._tr_fwd.mark_audio_segment_end()
164
- handle._buf_ch.close()
165
- # self._log_debug(f"tts finished synthesising {audio_duration:.2f}s of audio")
166
155
 
167
156
 
168
157
  @utils.log_exceptions(logger=logger)
@@ -173,27 +162,19 @@ async def _stream_synthesis_task(
173
162
 
174
163
  @utils.log_exceptions(logger=logger)
175
164
  async def _read_generated_audio_task():
176
- # start_time = time.time()
177
- # first_frame = True
178
- # audio_duration = 0.0
165
+ start_time = time.time()
166
+ first_frame = True
179
167
  async for audio in tts_stream:
180
- # if first_frame:
181
- # first_frame = False
182
- # dt = time.time() - start_time
183
- # self._log_debug(f"tts first frame in {dt:.2f}s (streamed)")
168
+ if first_frame:
169
+ first_frame = False
170
+ dt = time.time() - start_time
171
+ logger.debug(f"AgentOutput._stream_synthesis_task: TTFB in {dt:.2f}s")
184
172
 
185
- # audio_duration += frame.samples_per_channel / frame.sample_rate
186
- if handle._tr_fwd is not None:
173
+ if handle._tr_fwd and not handle._tr_fwd.closed:
187
174
  handle._tr_fwd.push_audio(audio.frame)
188
175
 
189
176
  handle._buf_ch.send_nowait(audio.frame)
190
177
 
191
- # we're only flushing once, so we know we can break at the end of the first segment
192
-
193
- # self._log_debug(
194
- # f"tts finished synthesising {audio_duration:.2f}s audio (streamed)"
195
- # )
196
-
197
178
  # otherwise, stream the text to the TTS
198
179
  tts_stream = handle._tts.stream()
199
180
  read_atask = asyncio.create_task(_read_generated_audio_task())
@@ -201,20 +182,20 @@ async def _stream_synthesis_task(
201
182
  try:
202
183
  async for seg in streamed_text:
203
184
  handle._collected_text += seg
204
- if handle._tr_fwd is not None:
185
+
186
+ if handle._tr_fwd and not handle._tr_fwd.closed:
205
187
  handle._tr_fwd.push_text(seg)
206
188
 
207
189
  tts_stream.push_text(seg)
208
-
209
190
  finally:
210
- if handle._tr_fwd is not None:
191
+ tts_stream.end_input()
192
+
193
+ if handle._tr_fwd and not handle._tr_fwd.closed:
211
194
  handle._tr_fwd.mark_text_segment_end()
212
195
 
213
- tts_stream.end_input()
214
196
  await read_atask
215
197
  await tts_stream.aclose()
216
198
 
217
- if handle._tr_fwd is not None:
199
+ if handle._tr_fwd and not handle._tr_fwd.closed:
200
+ # mark_audio_segment_end must be called *after* mart_text_segment_end
218
201
  handle._tr_fwd.mark_audio_segment_end()
219
-
220
- handle._buf_ch.close()
@@ -1,7 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import asyncio
4
- import contextlib
5
4
  from typing import AsyncIterable, Literal
6
5
 
7
6
  from livekit import rtc
@@ -41,8 +40,8 @@ class PlayoutHandle:
41
40
 
42
41
  self._interrupted = True
43
42
 
44
- def __await__(self):
45
- return self._done_fut.__await__()
43
+ def join(self) -> asyncio.Future:
44
+ return self._done_fut
46
45
 
47
46
 
48
47
  class CancellableAudioSource(utils.EventEmitter[EventTypes]):
@@ -89,6 +88,7 @@ class CancellableAudioSource(utils.EventEmitter[EventTypes]):
89
88
  self._playout_atask = asyncio.create_task(
90
89
  self._playout_task(self._playout_atask, handle)
91
90
  )
91
+
92
92
  return handle
93
93
 
94
94
  @utils.log_exceptions(logger=logger)
@@ -104,9 +104,9 @@ class CancellableAudioSource(utils.EventEmitter[EventTypes]):
104
104
 
105
105
  try:
106
106
  if old_task is not None:
107
- with contextlib.suppress(asyncio.CancelledError):
108
- old_task.cancel()
109
- await old_task
107
+ await utils.aio.gracefully_cancel(old_task)
108
+
109
+ logger.debug("CancellableAudioSource._playout_task: started")
110
110
 
111
111
  async for frame in handle._playout_source:
112
112
  if first_frame:
@@ -121,7 +121,7 @@ class CancellableAudioSource(utils.EventEmitter[EventTypes]):
121
121
  break
122
122
 
123
123
  # divide the frame by chunks of 20ms
124
- ms20 = frame.sample_rate // 100
124
+ ms20 = frame.sample_rate // 50
125
125
  i = 0
126
126
  while i < len(frame.data):
127
127
  if _should_break():
@@ -148,10 +148,12 @@ class CancellableAudioSource(utils.EventEmitter[EventTypes]):
148
148
  handle._time_played += rem / frame.sample_rate
149
149
  finally:
150
150
  if not first_frame:
151
- if handle._tr_fwd is not None:
152
- if not cancelled:
153
- handle._tr_fwd.segment_playout_finished()
151
+ if handle._tr_fwd is not None and not cancelled:
152
+ handle._tr_fwd.segment_playout_finished()
154
153
 
155
154
  self.emit("playout_stopped", cancelled)
156
155
 
157
- handle._done_fut.set_result(None)
156
+ handle._done_fut.set_result(None)
157
+ if handle._tr_fwd is not None:
158
+ await handle._tr_fwd.aclose()
159
+ logger.debug("CancellableAudioSource._playout_task: ended")
@@ -75,7 +75,7 @@ class HumanInput(utils.EventEmitter[EventTypes]):
75
75
  Subscribe to the participant microphone if found and not already subscribed.
76
76
  Do nothing if no track is found.
77
77
  """
78
- for publication in self._participant.tracks.values():
78
+ for publication in self._participant.track_publications.values():
79
79
  if publication.source != rtc.TrackSource.SOURCE_MICROPHONE:
80
80
  continue
81
81
 
@@ -216,7 +216,7 @@ class VoiceAssistant(utils.EventEmitter[EventTypes]):
216
216
  self._link_participant(participant)
217
217
  else:
218
218
  # no participant provided, try to find the first in the room
219
- for participant in self._room.participants.values():
219
+ for participant in self._room.remote_participants.values():
220
220
  self._link_participant(participant.identity)
221
221
  break
222
222
 
@@ -290,7 +290,7 @@ class VoiceAssistant(utils.EventEmitter[EventTypes]):
290
290
  self._link_participant(participant.identity)
291
291
 
292
292
  def _link_participant(self, identity: str) -> None:
293
- participant = self._room.participants_by_identity.get(identity)
293
+ participant = self._room.remote_participants.get(identity)
294
294
  if participant is None:
295
295
  logger.error("_link_participant must be called with a valid identity")
296
296
  return
@@ -312,7 +312,7 @@ class VoiceAssistant(utils.EventEmitter[EventTypes]):
312
312
 
313
313
  tv = 1.0
314
314
  if self._opts.allow_interruptions:
315
- tv = max(0, 1 - ev.probability)
315
+ tv = max(0.0, 1.0 - ev.probability)
316
316
  self._agent_output.audio_source.target_volume = tv
317
317
 
318
318
  smoothed_tv = self._agent_output.audio_source.smoothed_volume
@@ -471,6 +471,7 @@ class VoiceAssistant(utils.EventEmitter[EventTypes]):
471
471
  )
472
472
 
473
473
  async def _play_speech(self, speech_info: _SpeechInfo) -> None:
474
+ logger.debug("VoiceAssistant._play_speech started")
474
475
  MIN_TIME_PLAYED_FOR_COMMIT = 1.5
475
476
 
476
477
  assert (
@@ -485,7 +486,7 @@ class VoiceAssistant(utils.EventEmitter[EventTypes]):
485
486
  user_speech_commited = False
486
487
 
487
488
  play_handle = synthesis_handle.play()
488
- play_handle_fut = asyncio.ensure_future(play_handle)
489
+ join_fut = play_handle.join()
489
490
  self._playing_synthesis = synthesis_handle
490
491
 
491
492
  def _commit_user_message_if_needed() -> None:
@@ -507,7 +508,7 @@ class VoiceAssistant(utils.EventEmitter[EventTypes]):
507
508
  # really quickly (barely audible), we don't want to mark this question as "answered".
508
509
  if not is_using_tools and (
509
510
  play_handle.time_played < MIN_TIME_PLAYED_FOR_COMMIT
510
- and not play_handle_fut.done()
511
+ and not join_fut.done()
511
512
  ):
512
513
  return
513
514
 
@@ -519,9 +520,9 @@ class VoiceAssistant(utils.EventEmitter[EventTypes]):
519
520
  user_speech_commited = True
520
521
 
521
522
  # wait for the play_handle to finish and check every 1s if the user question should be committed
522
- while not play_handle_fut.done():
523
+ while not join_fut.done():
523
524
  await asyncio.wait(
524
- [play_handle_fut], return_when=asyncio.FIRST_COMPLETED, timeout=1.0
525
+ [join_fut], return_when=asyncio.FIRST_COMPLETED, timeout=1.0
525
526
  )
526
527
 
527
528
  _commit_user_message_if_needed()
@@ -579,7 +580,8 @@ class VoiceAssistant(utils.EventEmitter[EventTypes]):
579
580
  transcript=_llm_stream_to_str_iterable(answer_stream)
580
581
  )
581
582
  self._playing_synthesis = answer_synthesis
582
- await answer_synthesis.play()
583
+ play_handle = answer_synthesis.play()
584
+ await play_handle.join()
583
585
 
584
586
  collected_text = answer_synthesis.collected_text
585
587
  interrupted = answer_synthesis.interrupted
@@ -595,6 +597,8 @@ class VoiceAssistant(utils.EventEmitter[EventTypes]):
595
597
  else:
596
598
  self.emit("agent_speech_committed", msg)
597
599
 
600
+ logger.debug("VoiceAssistant._play_speech ended")
601
+
598
602
 
599
603
  async def _llm_stream_to_str_iterable(stream: LLMStream) -> AsyncIterable[str]:
600
604
  async for chunk in stream:
@@ -677,6 +681,7 @@ class _DeferredAnswerValidation:
677
681
  self._last_final_transcript = ""
678
682
  self._received_end_of_speech = False
679
683
  self._validate_fnc()
684
+ logger.debug("_DeferredAnswerValidation speech validated")
680
685
 
681
686
  def _run(self, delay: float) -> None:
682
687
  if self._validating_task is not None:
@@ -17,6 +17,7 @@ from __future__ import annotations
17
17
  import asyncio
18
18
  import contextlib
19
19
  import datetime
20
+ import multiprocessing as mp
20
21
  import os
21
22
  from dataclasses import dataclass, field
22
23
  from functools import reduce
@@ -35,7 +36,7 @@ from .job import JobAcceptArguments, JobContext, JobProcess, JobRequest, Running
35
36
  from .log import DEV_LEVEL, logger
36
37
  from .version import __version__
37
38
 
38
- MAX_RECONNECT_ATTEMPTS = 3.0
39
+ MAX_RECONNECT_ATTEMPTS = 3
39
40
  ASSIGNMENT_TIMEOUT = 7.5
40
41
  UPDATE_LOAD_INTERVAL = 10.0
41
42
 
@@ -43,9 +44,11 @@ UPDATE_LOAD_INTERVAL = 10.0
43
44
  def _default_initialize_process_fnc(proc: JobProcess) -> Any:
44
45
  return
45
46
 
47
+
46
48
  async def _default_shutdown_fnc(proc: JobContext) -> None:
47
49
  return
48
50
 
51
+
49
52
  async def _default_request_fnc(ctx: JobRequest) -> None:
50
53
  await ctx.accept()
51
54
 
@@ -122,12 +125,17 @@ class Worker(utils.EventEmitter[EventTypes]):
122
125
  self._pending_assignments: dict[str, asyncio.Future[agent.JobAssignment]] = {}
123
126
  self._close_future: asyncio.Future[None] | None = None
124
127
  self._msg_chan = utils.aio.Chan[agent.WorkerMessage](128, loop=self._loop)
128
+
129
+ # using spawn context for all platforms. We may have further optimizations for
130
+ # Linux with forkserver, but for now, this is the safest option
131
+ mp_ctx = mp.get_context("spawn")
125
132
  self._proc_pool = ipc.proc_pool.ProcPool(
126
133
  initialize_process_fnc=opts.prewarm_fnc,
127
134
  job_entrypoint_fnc=opts.entrypoint_fnc,
128
135
  job_shutdown_fnc=_default_shutdown_fnc,
129
136
  num_idle_processes=opts.num_idle_processes,
130
137
  loop=self._loop,
138
+ mp_ctx=mp_ctx,
131
139
  initialize_timeout=opts.initialize_process_timeout,
132
140
  close_timeout=opts.shutdown_process_timeout,
133
141
  )
@@ -240,7 +248,7 @@ class Worker(utils.EventEmitter[EventTypes]):
240
248
  """_queue_msg raises aio.ChanClosed when the worker is closing/closed"""
241
249
  if self._connecting:
242
250
  which = msg.WhichOneof("message")
243
- if which == "update_worker" and not msg.update_worker.metadata:
251
+ if which == "update_worker":
244
252
  return
245
253
  elif which == "ping":
246
254
  return
@@ -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.dev3
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
@@ -1,7 +1,7 @@
1
1
  click~=8.1
2
- livekit~=0.11
3
- livekit-api~=0.4
4
- livekit-protocol~=0.4
2
+ livekit~=0.12.0.dev1
3
+ livekit-api~=0.6.0
4
+ livekit-protocol~=0.6.0
5
5
  protobuf>=3
6
6
  pyjwt>=2.0.0
7
7
  types-protobuf<5,>=4
@@ -48,9 +48,9 @@ setuptools.setup(
48
48
  python_requires=">=3.9.0",
49
49
  install_requires=[
50
50
  "click~=8.1",
51
- "livekit~=0.11",
52
- "livekit-api~=0.4",
53
- "livekit-protocol~=0.4",
51
+ "livekit~=0.12.0.dev1",
52
+ "livekit-api~=0.6.0",
53
+ "livekit-protocol~=0.6.0",
54
54
  "protobuf>=3",
55
55
  "pyjwt>=2.0.0",
56
56
  "types-protobuf>=4,<5",