dvalle08 commited on
Commit
f70205c
·
1 Parent(s): 7ee632b

Enhance LiveKit integration: Add audio input and VAD configurations, update settings, and improve Streamlit app for better agent dispatch and error handling.

Browse files
.env.example CHANGED
@@ -21,3 +21,15 @@ LIVEKIT_API_SECRET=your_livekit_api_secret_here
21
  LIVEKIT_AGENT_NAME=open-voice-agent
22
  LIVEKIT_NUM_IDLE_PROCESSES=2
23
 
 
 
 
 
 
 
 
 
 
 
 
 
 
21
  LIVEKIT_AGENT_NAME=open-voice-agent
22
  LIVEKIT_NUM_IDLE_PROCESSES=2
23
 
24
+ # LiveKit Audio Input Configuration - OPTIMIZED FOR FALSE DETECTION FIX
25
+ LIVEKIT_SAMPLE_RATE=24000
26
+ LIVEKIT_NUM_CHANNELS=1
27
+ LIVEKIT_FRAME_SIZE_MS=20 # Smaller = faster VAD response, less latency
28
+ LIVEKIT_PRE_CONNECT_AUDIO=true
29
+ LIVEKIT_PRE_CONNECT_TIMEOUT=3.0
30
+
31
+ # Voice Activity Detection (VAD) Configuration - OPTIMIZED FOR FALSE DETECTION FIX
32
+ VAD_MIN_SPEECH_DURATION=0.25 # Require 250ms of speech before activation
33
+ VAD_MIN_SILENCE_DURATION=0.5 # Require 500ms of silence before deactivation
34
+ VAD_THRESHOLD=0.6 # Higher = less sensitive to noise (0.5 is default)
35
+
.streamlit/config.toml ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ [theme]
2
+ primaryColor="#5b8cff" # Blue accent (matches visualizer bars)
3
+ backgroundColor="#0f1115" # Dark background
4
+ secondaryBackgroundColor="#151923" # Panel background
5
+ textColor="#f2f2f2" # Light text
6
+ font="sans serif"
7
+
8
+ [server]
9
+ headless=true
pyproject.toml CHANGED
@@ -21,14 +21,14 @@ dependencies = [
21
  "livekit-agents[silero,turn-detector]>=1.3.3,<1.4",
22
  "livekit-plugins-noise-cancellation~=0.2",
23
  "livekit-api==1.1.0",
24
- "livekit-plugins-langchain",
25
- "langchain-nvidia-ai-endpoints",
26
- "langgraph",
27
- "pocket-tts",
28
- "scipy",
29
- "python-dotenv",
30
- "pydantic-settings",
31
- "streamlit",
32
  "transformers @ git+https://github.com/huggingface/transformers.git@7769f660935b5d48b73bf6711d0a78b6f8f98739",
33
-
34
  ]
 
21
  "livekit-agents[silero,turn-detector]>=1.3.3,<1.4",
22
  "livekit-plugins-noise-cancellation~=0.2",
23
  "livekit-api==1.1.0",
24
+ "livekit-plugins-langchain>=1.3.3",
25
+ "langchain-nvidia-ai-endpoints>=1.0.3",
26
+ "langgraph>=1.0.8",
27
+ "pocket-tts>=1.0.3",
28
+ "scipy>=1.17.0",
29
+ "python-dotenv>=1.2.1",
30
+ "pydantic-settings>=2.12.0",
31
+ "streamlit>=1.54.0",
32
  "transformers @ git+https://github.com/huggingface/transformers.git@7769f660935b5d48b73bf6711d0a78b6f8f98739",
33
+ "pytest>=8.0.0",
34
  ]
src/agent/agent.py CHANGED
@@ -34,7 +34,11 @@ async def session_handler(ctx: agents.JobContext) -> None:
34
  temperature=settings.voice.POCKET_TTS_TEMPERATURE,
35
  lsd_decode_steps=settings.voice.POCKET_TTS_LSD_DECODE_STEPS,
36
  ),
37
- vad=silero.VAD.load(),
 
 
 
 
38
  turn_detection=MultilingualModel(),
39
  )
40
  await session.start(
@@ -42,6 +46,11 @@ async def session_handler(ctx: agents.JobContext) -> None:
42
  agent=Assistant(),
43
  room_options=room_io.RoomOptions(
44
  audio_input=room_io.AudioInputOptions(
 
 
 
 
 
45
  noise_cancellation=lambda params: noise_cancellation.BVCTelephony()
46
  if params.participant.kind == rtc.ParticipantKind.PARTICIPANT_KIND_SIP
47
  else noise_cancellation.BVC(),
 
34
  temperature=settings.voice.POCKET_TTS_TEMPERATURE,
35
  lsd_decode_steps=settings.voice.POCKET_TTS_LSD_DECODE_STEPS,
36
  ),
37
+ vad=silero.VAD.load(
38
+ min_speech_duration=settings.voice.VAD_MIN_SPEECH_DURATION,
39
+ min_silence_duration=settings.voice.VAD_MIN_SILENCE_DURATION,
40
+ activation_threshold=settings.voice.VAD_THRESHOLD,
41
+ ),
42
  turn_detection=MultilingualModel(),
43
  )
44
  await session.start(
 
46
  agent=Assistant(),
47
  room_options=room_io.RoomOptions(
48
  audio_input=room_io.AudioInputOptions(
49
+ sample_rate=settings.voice.LIVEKIT_SAMPLE_RATE,
50
+ num_channels=settings.voice.LIVEKIT_NUM_CHANNELS,
51
+ frame_size_ms=settings.voice.LIVEKIT_FRAME_SIZE_MS,
52
+ pre_connect_audio=settings.voice.LIVEKIT_PRE_CONNECT_AUDIO,
53
+ pre_connect_audio_timeout=settings.voice.LIVEKIT_PRE_CONNECT_TIMEOUT,
54
  noise_cancellation=lambda params: noise_cancellation.BVCTelephony()
55
  if params.participant.kind == rtc.ParticipantKind.PARTICIPANT_KIND_SIP
56
  else noise_cancellation.BVC(),
src/core/settings.py CHANGED
@@ -67,10 +67,56 @@ class VoiceSettings(CoreSettings):
67
  description="LSD decoding steps (higher = better quality, slower)",
68
  )
69
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
70
 
71
  class LLMSettings(CoreSettings):
72
  NVIDIA_API_KEY: Optional[str] = Field(default=None)
73
- NVIDIA_MODEL: str = Field(default="meta/llama-3.1-8b-instruct")
74
 
75
  LLM_TEMPERATURE: float = Field(default=0.7, ge=0.0, le=2.0)
76
  LLM_MAX_TOKENS: int = Field(default=1024, gt=0)
@@ -81,7 +127,7 @@ class LiveKitSettings(CoreSettings):
81
  LIVEKIT_API_KEY: Optional[str] = Field(default=None)
82
  LIVEKIT_API_SECRET: Optional[str] = Field(default=None)
83
  LIVEKIT_AGENT_NAME: str = Field(default="open-voice-agent")
84
- LIVEKIT_NUM_IDLE_PROCESSES: int = Field(default=2, ge=0)
85
 
86
 
87
  class Settings(CoreSettings):
 
67
  description="LSD decoding steps (higher = better quality, slower)",
68
  )
69
 
70
+ # LiveKit Audio Input Settings
71
+ LIVEKIT_SAMPLE_RATE: int = Field(
72
+ default=24000,
73
+ description="Audio input sample rate (Hz)",
74
+ )
75
+ LIVEKIT_NUM_CHANNELS: int = Field(
76
+ default=1,
77
+ description="Number of audio input channels (1=mono)",
78
+ )
79
+ LIVEKIT_FRAME_SIZE_MS: int = Field(
80
+ default=20,
81
+ ge=10,
82
+ le=100,
83
+ description="Audio frame size in milliseconds (smaller = faster VAD response)",
84
+ )
85
+ LIVEKIT_PRE_CONNECT_AUDIO: bool = Field(
86
+ default=True,
87
+ description="Pre-connect audio before room join",
88
+ )
89
+ LIVEKIT_PRE_CONNECT_TIMEOUT: float = Field(
90
+ default=3.0,
91
+ ge=1.0,
92
+ le=10.0,
93
+ description="Timeout for pre-connect audio (seconds)",
94
+ )
95
+
96
+ # Voice Activity Detection Settings
97
+ VAD_MIN_SPEECH_DURATION: float = Field(
98
+ default=0.25,
99
+ ge=0.1,
100
+ le=1.0,
101
+ description="Minimum speech duration (seconds) before VAD activation",
102
+ )
103
+ VAD_MIN_SILENCE_DURATION: float = Field(
104
+ default=0.5,
105
+ ge=0.1,
106
+ le=2.0,
107
+ description="Minimum silence duration (seconds) before VAD deactivation",
108
+ )
109
+ VAD_THRESHOLD: float = Field(
110
+ default=0.6,
111
+ ge=0.0,
112
+ le=1.0,
113
+ description="VAD activation threshold (higher = less sensitive, 0.5 is Silero default)",
114
+ )
115
+
116
 
117
  class LLMSettings(CoreSettings):
118
  NVIDIA_API_KEY: Optional[str] = Field(default=None)
119
+ NVIDIA_MODEL: str = Field(default="openai/gpt-oss-20b") # "meta/llama-3.1-8b-instruct"
120
 
121
  LLM_TEMPERATURE: float = Field(default=0.7, ge=0.0, le=2.0)
122
  LLM_MAX_TOKENS: int = Field(default=1024, gt=0)
 
127
  LIVEKIT_API_KEY: Optional[str] = Field(default=None)
128
  LIVEKIT_API_SECRET: Optional[str] = Field(default=None)
129
  LIVEKIT_AGENT_NAME: str = Field(default="open-voice-agent")
130
+ LIVEKIT_NUM_IDLE_PROCESSES: int = Field(default=3, ge=0)
131
 
132
 
133
  class Settings(CoreSettings):
src/streamlit_app.py CHANGED
@@ -34,21 +34,28 @@ def main() -> None:
34
 
35
  if not settings.livekit.LIVEKIT_URL:
36
  st.error("LIVEKIT_URL is not set in the environment.")
37
- return
38
  if not settings.livekit.LIVEKIT_API_KEY or not settings.livekit.LIVEKIT_API_SECRET:
39
  st.error("LIVEKIT_API_KEY or LIVEKIT_API_SECRET is not set.")
40
- return
41
 
42
- default_room = f"voice-{uuid4().hex[:8]}"
43
- room_name = st.text_input("Room name", value=st.session_state.get("room_name", default_room))
44
- st.session_state["room_name"] = room_name
45
 
46
- start = st.button("Start session", type="primary")
47
- if start or "token" not in st.session_state:
48
- token_data = create_room_token(room_name=room_name)
49
- st.session_state["token"] = token_data.token
50
- st.session_state["agent_dispatched"] = False
51
 
 
 
 
 
 
 
 
 
 
 
 
52
  if not st.session_state.get("agent_dispatched"):
53
  try:
54
  dispatch_agent_sync(
@@ -58,14 +65,9 @@ def main() -> None:
58
  st.session_state["agent_dispatched"] = True
59
  except Exception as exc:
60
  st.error(f"Failed to dispatch agent: {exc}")
61
- return
62
-
63
- token = st.session_state.get("token")
64
- if not token:
65
- st.info("Click Start session to generate a LiveKit token.")
66
- return
67
 
68
- render_client(token=token, livekit_url=settings.livekit.LIVEKIT_URL)
69
 
70
 
71
  if __name__ == "__main__":
 
34
 
35
  if not settings.livekit.LIVEKIT_URL:
36
  st.error("LIVEKIT_URL is not set in the environment.")
37
+ st.stop()
38
  if not settings.livekit.LIVEKIT_API_KEY or not settings.livekit.LIVEKIT_API_SECRET:
39
  st.error("LIVEKIT_API_KEY or LIVEKIT_API_SECRET is not set.")
40
+ st.stop()
41
 
42
+ # Auto-generate room name once
43
+ if "room_name" not in st.session_state:
44
+ st.session_state["room_name"] = f"voice-{uuid4().hex[:8]}"
45
 
46
+ room_name = st.session_state["room_name"]
 
 
 
 
47
 
48
+ # Auto-create token once
49
+ if "token" not in st.session_state:
50
+ try:
51
+ token_data = create_room_token(room_name=room_name)
52
+ st.session_state["token"] = token_data.token
53
+ st.session_state["agent_dispatched"] = False
54
+ except Exception as exc:
55
+ st.error(f"Failed to create room token: {exc}")
56
+ st.stop()
57
+
58
+ # Auto-dispatch agent once
59
  if not st.session_state.get("agent_dispatched"):
60
  try:
61
  dispatch_agent_sync(
 
65
  st.session_state["agent_dispatched"] = True
66
  except Exception as exc:
67
  st.error(f"Failed to dispatch agent: {exc}")
68
+ st.stop()
 
 
 
 
 
69
 
70
+ render_client(token=st.session_state["token"], livekit_url=settings.livekit.LIVEKIT_URL)
71
 
72
 
73
  if __name__ == "__main__":
src/ui/index.html CHANGED
@@ -6,7 +6,7 @@
6
  name="viewport"
7
  content="width=device-width, initial-scale=1, maximum-scale=1"
8
  />
9
- <title>LiveKit Voice UI</title>
10
  <style>
11
  :root {
12
  color-scheme: light dark;
@@ -17,22 +17,31 @@
17
  padding: 16px;
18
  background: #0f1115;
19
  color: #f2f2f2;
 
 
 
 
20
  }
21
- .panel {
22
- border: 1px solid #2a2f3a;
23
- border-radius: 12px;
24
- padding: 16px;
25
- background: #151923;
26
  }
27
  .status {
28
  margin-bottom: 12px;
29
  font-size: 14px;
30
  color: #b9c0d4;
31
  }
 
 
 
 
 
 
 
32
  .controls {
33
  display: flex;
34
  gap: 8px;
35
- margin-top: 12px;
36
  }
37
  button {
38
  padding: 8px 12px;
@@ -46,12 +55,6 @@
46
  opacity: 0.6;
47
  cursor: not-allowed;
48
  }
49
- canvas {
50
- width: 100%;
51
- height: 140px;
52
- background: #0b0e14;
53
- border-radius: 8px;
54
- }
55
  .note {
56
  font-size: 12px;
57
  color: #8b93a7;
@@ -60,7 +63,7 @@
60
  </style>
61
  </head>
62
  <body>
63
- <div class="panel">
64
  <div id="status" class="status">Idle</div>
65
  <canvas id="wave" width="800" height="140"></canvas>
66
  <div class="controls">
@@ -69,7 +72,7 @@
69
  <button id="mute" disabled>Mute mic</button>
70
  </div>
71
  <div class="note">
72
- Mic audio is streamed to LiveKit. Remote agent audio plays automatically.
73
  </div>
74
  <audio id="remote-audio" autoplay></audio>
75
  </div>
 
6
  name="viewport"
7
  content="width=device-width, initial-scale=1, maximum-scale=1"
8
  />
9
+ <title>Open Voice Agent</title>
10
  <style>
11
  :root {
12
  color-scheme: light dark;
 
17
  padding: 16px;
18
  background: #0f1115;
19
  color: #f2f2f2;
20
+ display: flex;
21
+ justify-content: center;
22
+ align-items: center;
23
+ min-height: 100vh;
24
  }
25
+ .container {
26
+ max-width: 800px;
27
+ width: 100%;
 
 
28
  }
29
  .status {
30
  margin-bottom: 12px;
31
  font-size: 14px;
32
  color: #b9c0d4;
33
  }
34
+ canvas {
35
+ width: 100%;
36
+ height: 140px;
37
+ background: #0f1115;
38
+ border-radius: 8px;
39
+ margin-bottom: 12px;
40
+ }
41
  .controls {
42
  display: flex;
43
  gap: 8px;
44
+ margin-bottom: 12px;
45
  }
46
  button {
47
  padding: 8px 12px;
 
55
  opacity: 0.6;
56
  cursor: not-allowed;
57
  }
 
 
 
 
 
 
58
  .note {
59
  font-size: 12px;
60
  color: #8b93a7;
 
63
  </style>
64
  </head>
65
  <body>
66
+ <div class="container">
67
  <div id="status" class="status">Idle</div>
68
  <canvas id="wave" width="800" height="140"></canvas>
69
  <div class="controls">
 
72
  <button id="mute" disabled>Mute mic</button>
73
  </div>
74
  <div class="note">
75
+ Powered by open source models (Moonshine STT, NVIDIA LLM, Pocket TTS) running on LiveKit infrastructure.
76
  </div>
77
  <audio id="remote-audio" autoplay></audio>
78
  </div>
src/ui/main.js CHANGED
@@ -43,8 +43,10 @@ function drawWave() {
43
  const value = dataArray[i * step] || 0;
44
  const normalized = value / 255;
45
  const barHeight = Math.max(6, normalized * canvas.height);
 
 
 
46
  const x = i * barWidth;
47
- const y = canvas.height - barHeight;
48
  ctx.fillStyle = "#5b8cff";
49
  ctx.fillRect(x + 2, y, barWidth - 4, barHeight);
50
  }
 
43
  const value = dataArray[i * step] || 0;
44
  const normalized = value / 255;
45
  const barHeight = Math.max(6, normalized * canvas.height);
46
+ const halfHeight = barHeight / 2;
47
+ const centerY = canvas.height / 2;
48
+ const y = centerY - halfHeight;
49
  const x = i * barWidth;
 
50
  ctx.fillStyle = "#5b8cff";
51
  ctx.fillRect(x + 2, y, barWidth - 4, barHeight);
52
  }
uv.lock CHANGED
@@ -549,6 +549,15 @@ wheels = [
549
  { url = "https://files.pythonhosted.org/packages/fa/5e/f8e9a1d23b9c20a551a8a02ea3637b4642e22c2626e3a13a9a29cdea99eb/importlib_metadata-8.7.1-py3-none-any.whl", hash = "sha256:5a1f80bf1daa489495071efbb095d75a634cf28a8bc299581244063b53176151", size = 27865, upload-time = "2025-12-21T10:00:18.329Z" },
550
  ]
551
 
 
 
 
 
 
 
 
 
 
552
  [[package]]
553
  name = "jinja2"
554
  version = "3.1.6"
@@ -1201,6 +1210,7 @@ dependencies = [
1201
  { name = "opentelemetry-sdk" },
1202
  { name = "pocket-tts" },
1203
  { name = "pydantic-settings" },
 
1204
  { name = "python-dotenv" },
1205
  { name = "scipy" },
1206
  { name = "streamlit" },
@@ -1209,20 +1219,21 @@ dependencies = [
1209
 
1210
  [package.metadata]
1211
  requires-dist = [
1212
- { name = "langchain-nvidia-ai-endpoints" },
1213
- { name = "langgraph" },
1214
  { name = "livekit-agents", extras = ["silero", "turn-detector"], specifier = ">=1.3.3,<1.4" },
1215
  { name = "livekit-api", specifier = "==1.1.0" },
1216
- { name = "livekit-plugins-langchain" },
1217
  { name = "livekit-plugins-noise-cancellation", specifier = "~=0.2" },
1218
  { name = "opentelemetry-api", specifier = ">=1.34.0,<1.35.0" },
1219
  { name = "opentelemetry-exporter-otlp", specifier = ">=1.34.0,<1.35.0" },
1220
  { name = "opentelemetry-sdk", specifier = ">=1.34.0,<1.35.0" },
1221
- { name = "pocket-tts" },
1222
- { name = "pydantic-settings" },
1223
- { name = "python-dotenv" },
1224
- { name = "scipy" },
1225
- { name = "streamlit" },
 
1226
  { name = "transformers", git = "https://github.com/huggingface/transformers.git?rev=7769f660935b5d48b73bf6711d0a78b6f8f98739" },
1227
  ]
1228
 
@@ -1447,6 +1458,15 @@ wheels = [
1447
  { url = "https://files.pythonhosted.org/packages/5d/cf/881b457eccacac9e5b2ddd97d5071fb6d668307c57cbf4e3b5278e06e536/pillow-12.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:65b80c1ee7e14a87d6a068dd3b0aea268ffcabfe0498d38661b00c5b4b22e74c", size = 2452612, upload-time = "2026-01-02T09:11:29.309Z" },
1448
  ]
1449
 
 
 
 
 
 
 
 
 
 
1450
  [[package]]
1451
  name = "pocket-tts"
1452
  version = "1.0.3"
@@ -1658,6 +1678,22 @@ wheels = [
1658
  { url = "https://files.pythonhosted.org/packages/5a/dc/491b7661614ab97483abf2056be1deee4dc2490ecbf7bff9ab5cdbac86e1/pyreadline3-3.5.4-py3-none-any.whl", hash = "sha256:eaf8e6cc3c49bcccf145fc6067ba8643d1df34d604a1ec0eccbf7a18e6d3fae6", size = 83178, upload-time = "2024-09-19T02:40:08.598Z" },
1659
  ]
1660
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1661
  [[package]]
1662
  name = "python-dateutil"
1663
  version = "2.9.0.post0"
 
549
  { url = "https://files.pythonhosted.org/packages/fa/5e/f8e9a1d23b9c20a551a8a02ea3637b4642e22c2626e3a13a9a29cdea99eb/importlib_metadata-8.7.1-py3-none-any.whl", hash = "sha256:5a1f80bf1daa489495071efbb095d75a634cf28a8bc299581244063b53176151", size = 27865, upload-time = "2025-12-21T10:00:18.329Z" },
550
  ]
551
 
552
+ [[package]]
553
+ name = "iniconfig"
554
+ version = "2.3.0"
555
+ source = { registry = "https://pypi.org/simple" }
556
+ sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" }
557
+ wheels = [
558
+ { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
559
+ ]
560
+
561
  [[package]]
562
  name = "jinja2"
563
  version = "3.1.6"
 
1210
  { name = "opentelemetry-sdk" },
1211
  { name = "pocket-tts" },
1212
  { name = "pydantic-settings" },
1213
+ { name = "pytest" },
1214
  { name = "python-dotenv" },
1215
  { name = "scipy" },
1216
  { name = "streamlit" },
 
1219
 
1220
  [package.metadata]
1221
  requires-dist = [
1222
+ { name = "langchain-nvidia-ai-endpoints", specifier = ">=1.0.3" },
1223
+ { name = "langgraph", specifier = ">=1.0.8" },
1224
  { name = "livekit-agents", extras = ["silero", "turn-detector"], specifier = ">=1.3.3,<1.4" },
1225
  { name = "livekit-api", specifier = "==1.1.0" },
1226
+ { name = "livekit-plugins-langchain", specifier = ">=1.3.3" },
1227
  { name = "livekit-plugins-noise-cancellation", specifier = "~=0.2" },
1228
  { name = "opentelemetry-api", specifier = ">=1.34.0,<1.35.0" },
1229
  { name = "opentelemetry-exporter-otlp", specifier = ">=1.34.0,<1.35.0" },
1230
  { name = "opentelemetry-sdk", specifier = ">=1.34.0,<1.35.0" },
1231
+ { name = "pocket-tts", specifier = ">=1.0.3" },
1232
+ { name = "pydantic-settings", specifier = ">=2.12.0" },
1233
+ { name = "pytest", specifier = ">=8.0.0" },
1234
+ { name = "python-dotenv", specifier = ">=1.2.1" },
1235
+ { name = "scipy", specifier = ">=1.17.0" },
1236
+ { name = "streamlit", specifier = ">=1.54.0" },
1237
  { name = "transformers", git = "https://github.com/huggingface/transformers.git?rev=7769f660935b5d48b73bf6711d0a78b6f8f98739" },
1238
  ]
1239
 
 
1458
  { url = "https://files.pythonhosted.org/packages/5d/cf/881b457eccacac9e5b2ddd97d5071fb6d668307c57cbf4e3b5278e06e536/pillow-12.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:65b80c1ee7e14a87d6a068dd3b0aea268ffcabfe0498d38661b00c5b4b22e74c", size = 2452612, upload-time = "2026-01-02T09:11:29.309Z" },
1459
  ]
1460
 
1461
+ [[package]]
1462
+ name = "pluggy"
1463
+ version = "1.6.0"
1464
+ source = { registry = "https://pypi.org/simple" }
1465
+ sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" }
1466
+ wheels = [
1467
+ { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
1468
+ ]
1469
+
1470
  [[package]]
1471
  name = "pocket-tts"
1472
  version = "1.0.3"
 
1678
  { url = "https://files.pythonhosted.org/packages/5a/dc/491b7661614ab97483abf2056be1deee4dc2490ecbf7bff9ab5cdbac86e1/pyreadline3-3.5.4-py3-none-any.whl", hash = "sha256:eaf8e6cc3c49bcccf145fc6067ba8643d1df34d604a1ec0eccbf7a18e6d3fae6", size = 83178, upload-time = "2024-09-19T02:40:08.598Z" },
1679
  ]
1680
 
1681
+ [[package]]
1682
+ name = "pytest"
1683
+ version = "9.0.2"
1684
+ source = { registry = "https://pypi.org/simple" }
1685
+ dependencies = [
1686
+ { name = "colorama", marker = "sys_platform == 'win32'" },
1687
+ { name = "iniconfig" },
1688
+ { name = "packaging" },
1689
+ { name = "pluggy" },
1690
+ { name = "pygments" },
1691
+ ]
1692
+ sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" }
1693
+ wheels = [
1694
+ { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" },
1695
+ ]
1696
+
1697
  [[package]]
1698
  name = "python-dateutil"
1699
  version = "2.9.0.post0"