livekit-plugins-volcenginee 1.3.12__tar.gz → 1.3.13__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 (14) hide show
  1. {livekit_plugins_volcenginee-1.3.12 → livekit_plugins_volcenginee-1.3.13}/PKG-INFO +61 -2
  2. {livekit_plugins_volcenginee-1.3.12 → livekit_plugins_volcenginee-1.3.13}/README.md +59 -0
  3. {livekit_plugins_volcenginee-1.3.12 → livekit_plugins_volcenginee-1.3.13}/livekit/plugins/volcengine/stt.py +147 -47
  4. livekit_plugins_volcenginee-1.3.13/livekit/plugins/volcengine/version.py +1 -0
  5. {livekit_plugins_volcenginee-1.3.12 → livekit_plugins_volcenginee-1.3.13}/pyproject.toml +2 -4
  6. livekit_plugins_volcenginee-1.3.12/livekit/plugins/volcengine/version.py +0 -1
  7. {livekit_plugins_volcenginee-1.3.12 → livekit_plugins_volcenginee-1.3.13}/.gitignore +0 -0
  8. {livekit_plugins_volcenginee-1.3.12 → livekit_plugins_volcenginee-1.3.13}/livekit/plugins/volcengine/__init__.py +0 -0
  9. {livekit_plugins_volcenginee-1.3.12 → livekit_plugins_volcenginee-1.3.13}/livekit/plugins/volcengine/llm.py +0 -0
  10. {livekit_plugins_volcenginee-1.3.12 → livekit_plugins_volcenginee-1.3.13}/livekit/plugins/volcengine/log.py +0 -0
  11. {livekit_plugins_volcenginee-1.3.12 → livekit_plugins_volcenginee-1.3.13}/livekit/plugins/volcengine/py.typed +0 -0
  12. {livekit_plugins_volcenginee-1.3.12 → livekit_plugins_volcenginee-1.3.13}/livekit/plugins/volcengine/realtime.py +0 -0
  13. {livekit_plugins_volcenginee-1.3.12 → livekit_plugins_volcenginee-1.3.13}/livekit/plugins/volcengine/tts.py +0 -0
  14. {livekit_plugins_volcenginee-1.3.12 → livekit_plugins_volcenginee-1.3.13}/livekit/plugins/volcengine/utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: livekit-plugins-volcenginee
3
- Version: 1.3.12
3
+ Version: 1.3.13
4
4
  Summary: LiveKit Agent Plugins for Volcengine
5
5
  Author-email: linsanzhu <2287225730@qq.com>
6
6
  Keywords: audio,livekit,realtime,video,webrtc
@@ -13,7 +13,7 @@ Classifier: Topic :: Multimedia :: Sound/Audio
13
13
  Classifier: Topic :: Multimedia :: Video
14
14
  Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
15
15
  Requires-Python: >=3.10
16
- Requires-Dist: livekit-agents~=1.5.13
16
+ Requires-Dist: livekit-agents>=1.5.13
17
17
  Requires-Dist: numpy
18
18
  Requires-Dist: openai>=1.80.0
19
19
  Requires-Dist: osc-data~=0.3.1
@@ -434,12 +434,62 @@ volcengine.STT(
434
434
  enable_itn: bool = False,
435
435
  enable_punc: bool = True,
436
436
  enable_ddc: bool = False,
437
+ enable_diarization: bool = False, # 开启说话人分离(同时启用性别识别)
437
438
  vad_segment_duration: int = 3000,
438
439
  end_window_size: int = 500,
439
440
  force_to_speech_time: int = 1000,
440
441
  )
441
442
  ```
442
443
 
444
+ #### 说话人分离(Diarization)
445
+
446
+ 把 `enable_diarization=True` 打开后,插件会:
447
+
448
+ 1. 在 `STTCapabilities.diarization` 上声明能力,符合
449
+ [LiveKit STT 文档](https://docs.livekit.io/agents/models/stt/#speaker-diarization)
450
+ 要求的三件事之一。
451
+ 2. 在每条 `SpeechData` 上填上来自服务端 `additions.speaker` 的 `speaker_id`(形如
452
+ `"speaker_1"`)。
453
+ 3. 在 `SpeechData.metadata["gender"]` 里带上 `additions.gender`(按官方文档,性别
454
+ 只在 `definite=true` 的分句上返回,所以中间结果不会有)。
455
+ 4. 按分句拆分 `utterances` 数组,每段独立事件(START → INTERIM/FINAL → END),
456
+ 而不是把多段合并到一个事件里——这样下游的 `MultiSpeakerAdapter` 能在段粒度上
457
+ 决定谁是主说话人、要不要抑制背景。
458
+
459
+ > 注意:插件只做"原始说话人分离"的暴露,**不**做主说话人判断或背景抑制。这两个
460
+ > 策略属于 `MultiSpeakerAdapter`。典型用法:
461
+
462
+ ```python
463
+ from livekit.agents import Agent, AgentSession, JobContext, cli, WorkerOptions
464
+ from livekit.agents.stt import MultiSpeakerAdapter
465
+ from livekit.plugins import volcengine
466
+
467
+
468
+ async def entry_point(ctx: JobContext):
469
+ raw_stt = volcengine.STT(
470
+ api_key="your_api_key",
471
+ enable_diarization=True, # 暴露每段 speaker_id + gender
472
+ )
473
+ # 用官方适配器做主说话人检测 / 背景抑制
474
+ stt = MultiSpeakerAdapter(
475
+ raw_stt,
476
+ detect_primary_speaker=True, # 默认 True:识别谁是主说话人
477
+ suppress_background_speaker=False, # True 则只把主说话人文本交给 agent
478
+ )
479
+
480
+ agent = Agent(instructions="你是一个友好的助手。")
481
+ session = AgentSession(stt=stt, llm=..., tts=...)
482
+ await session.start(agent=agent, room=ctx.room)
483
+ await ctx.connect()
484
+
485
+
486
+ if __name__ == "__main__":
487
+ cli.run_app(WorkerOptions(entrypoint_fnc=entry_point))
488
+ ```
489
+
490
+ 当 `MultiSpeakerAdapter.suppress_background_speaker=True` 时,下游 agent 只会
491
+ 看到主说话人的分句,其它说话人(如旁人的声音、背景里的电视)会直接被吞掉。
492
+
443
493
  ### LLM (大语言模型)
444
494
 
445
495
  ```python
@@ -582,6 +632,15 @@ A: 根据您的应用需求选择合适的版本:
582
632
 
583
633
  ## 📝 更新日志
584
634
 
635
+ ### 最新变更
636
+ - `volcengine.STT` 新增 `enable_diarization` 参数(默认关闭)。开启后:
637
+ - `STTCapabilities.diarization = True`,可被 `MultiSpeakerAdapter` 包装做主说话人
638
+ 检测 / 背景抑制。
639
+ - 每段 `SpeechData` 携带 `speaker_id`(来自 `additions.speaker`),性别写入
640
+ `SpeechData.metadata["gender"]`(按官方文档,gender 仅在 `definite=true` 的分句
641
+ 上返回)。
642
+ - `utterances[]` 数组按分句拆分事件,方便下游在段粒度上做判断。
643
+
585
644
  ### v1.5.4 适配
586
645
  - 升级 `livekit-agents` 到 `1.5.4`
587
646
  - Python 要求调整为 `>=3.10`
@@ -407,12 +407,62 @@ volcengine.STT(
407
407
  enable_itn: bool = False,
408
408
  enable_punc: bool = True,
409
409
  enable_ddc: bool = False,
410
+ enable_diarization: bool = False, # 开启说话人分离(同时启用性别识别)
410
411
  vad_segment_duration: int = 3000,
411
412
  end_window_size: int = 500,
412
413
  force_to_speech_time: int = 1000,
413
414
  )
414
415
  ```
415
416
 
417
+ #### 说话人分离(Diarization)
418
+
419
+ 把 `enable_diarization=True` 打开后,插件会:
420
+
421
+ 1. 在 `STTCapabilities.diarization` 上声明能力,符合
422
+ [LiveKit STT 文档](https://docs.livekit.io/agents/models/stt/#speaker-diarization)
423
+ 要求的三件事之一。
424
+ 2. 在每条 `SpeechData` 上填上来自服务端 `additions.speaker` 的 `speaker_id`(形如
425
+ `"speaker_1"`)。
426
+ 3. 在 `SpeechData.metadata["gender"]` 里带上 `additions.gender`(按官方文档,性别
427
+ 只在 `definite=true` 的分句上返回,所以中间结果不会有)。
428
+ 4. 按分句拆分 `utterances` 数组,每段独立事件(START → INTERIM/FINAL → END),
429
+ 而不是把多段合并到一个事件里——这样下游的 `MultiSpeakerAdapter` 能在段粒度上
430
+ 决定谁是主说话人、要不要抑制背景。
431
+
432
+ > 注意:插件只做"原始说话人分离"的暴露,**不**做主说话人判断或背景抑制。这两个
433
+ > 策略属于 `MultiSpeakerAdapter`。典型用法:
434
+
435
+ ```python
436
+ from livekit.agents import Agent, AgentSession, JobContext, cli, WorkerOptions
437
+ from livekit.agents.stt import MultiSpeakerAdapter
438
+ from livekit.plugins import volcengine
439
+
440
+
441
+ async def entry_point(ctx: JobContext):
442
+ raw_stt = volcengine.STT(
443
+ api_key="your_api_key",
444
+ enable_diarization=True, # 暴露每段 speaker_id + gender
445
+ )
446
+ # 用官方适配器做主说话人检测 / 背景抑制
447
+ stt = MultiSpeakerAdapter(
448
+ raw_stt,
449
+ detect_primary_speaker=True, # 默认 True:识别谁是主说话人
450
+ suppress_background_speaker=False, # True 则只把主说话人文本交给 agent
451
+ )
452
+
453
+ agent = Agent(instructions="你是一个友好的助手。")
454
+ session = AgentSession(stt=stt, llm=..., tts=...)
455
+ await session.start(agent=agent, room=ctx.room)
456
+ await ctx.connect()
457
+
458
+
459
+ if __name__ == "__main__":
460
+ cli.run_app(WorkerOptions(entrypoint_fnc=entry_point))
461
+ ```
462
+
463
+ 当 `MultiSpeakerAdapter.suppress_background_speaker=True` 时,下游 agent 只会
464
+ 看到主说话人的分句,其它说话人(如旁人的声音、背景里的电视)会直接被吞掉。
465
+
416
466
  ### LLM (大语言模型)
417
467
 
418
468
  ```python
@@ -555,6 +605,15 @@ A: 根据您的应用需求选择合适的版本:
555
605
 
556
606
  ## 📝 更新日志
557
607
 
608
+ ### 最新变更
609
+ - `volcengine.STT` 新增 `enable_diarization` 参数(默认关闭)。开启后:
610
+ - `STTCapabilities.diarization = True`,可被 `MultiSpeakerAdapter` 包装做主说话人
611
+ 检测 / 背景抑制。
612
+ - 每段 `SpeechData` 携带 `speaker_id`(来自 `additions.speaker`),性别写入
613
+ `SpeechData.metadata["gender"]`(按官方文档,gender 仅在 `definite=true` 的分句
614
+ 上返回)。
615
+ - `utterances[]` 数组按分句拆分事件,方便下游在段粒度上做判断。
616
+
558
617
  ### v1.5.4 适配
559
618
  - 升级 `livekit-agents` 到 `1.5.4`
560
619
  - Python 要求调整为 `>=3.10`
@@ -98,6 +98,16 @@ class STTOptions:
98
98
  end_window_size: int = 500
99
99
  force_to_speech_time: int = 1000
100
100
 
101
+ # Speaker diarization. When ``enable_diarization`` is true, the
102
+ # plugin asks the Volcengine ASR for per-utterance ``speaker`` ids
103
+ # and exposes them on each emitted ``SpeechData.speaker_id`` so
104
+ # that LiveKit's ``MultiSpeakerAdapter`` can do main-speaker
105
+ # detection or background-speaker suppression on top. Gender
106
+ # detection is bundled in — the Volcengine docs tie it to
107
+ # ``enable_speaker_info``, so once diarization is on we always
108
+ # request gender and surface it via ``SpeechData.metadata``.
109
+ enable_diarization: bool = False
110
+
101
111
  def get_ws_url(self):
102
112
  return self.base_url
103
113
 
@@ -119,9 +129,9 @@ class STTOptions:
119
129
  "enable_punc": self.enable_punc,
120
130
  "enable_ddc": self.enable_ddc,
121
131
  "enable_nonstream": True,
122
- "enable_speaker_info": True,
132
+ "enable_speaker_info": self.enable_diarization,
123
133
  "ssd_version": "200",
124
- "enable_gender_detection": True,
134
+ "enable_gender_detection": self.enable_diarization,
125
135
  "show_utterance": self.show_utterance,
126
136
  "result_type": self.result_type,
127
137
  "vad_segment_duration": self.vad_segment_duration,
@@ -191,17 +201,25 @@ class STT(stt.STT):
191
201
  enable_itn: bool = False,
192
202
  enable_punc: bool = True,
193
203
  enable_ddc: bool = False,
204
+ enable_diarization: bool = False,
194
205
  vad_segment_duration: int = 3000,
195
206
  end_window_size: int = 500,
196
207
  force_to_speech_time: int = 1000,
197
208
  http_session: aiohttp.ClientSession | None = None,
198
209
  interim_results: bool = True,
199
210
  ) -> None:
200
- super().__init__(
201
- capabilities=stt.STTCapabilities(
202
- streaming=True, interim_results=interim_results
203
- )
204
- )
211
+ # Per the LiveKit STT API contract, a plugin that exposes
212
+ # per-segment speaker ids must set ``diarization=True`` on its
213
+ # capabilities so the framework knows it can wrap us in
214
+ # ``MultiSpeakerAdapter`` for primary-speaker detection /
215
+ # background-speaker suppression.
216
+ capabilities_kwargs: dict = {
217
+ "streaming": True,
218
+ "interim_results": interim_results,
219
+ "diarization": enable_diarization,
220
+ }
221
+
222
+ super().__init__(capabilities=stt.STTCapabilities(**capabilities_kwargs))
205
223
 
206
224
  api_key = api_key if is_given(api_key) else os.getenv("VOLCENGINE_STT_API_KEY")
207
225
  if api_key is None:
@@ -218,6 +236,7 @@ class STT(stt.STT):
218
236
  enable_itn=enable_itn,
219
237
  enable_punc=enable_punc,
220
238
  enable_ddc=enable_ddc,
239
+ enable_diarization=enable_diarization,
221
240
  vad_segment_duration=vad_segment_duration,
222
241
  end_window_size=end_window_size,
223
242
  force_to_speech_time=force_to_speech_time,
@@ -436,69 +455,127 @@ class SpeechStream(stt.SpeechStream):
436
455
  text = result.get("text", "")
437
456
  if text == "":
438
457
  return
458
+
439
459
  utterances = result.get("utterances", [])
440
460
  if len(utterances) == 0:
441
461
  return
442
- language = self._opts.language
443
- definite = utterances[0].get("definite", False)
444
- start_time = utterances[0].get("start_time", 0.0)
445
- end_time = utterances[0].get("end_time", 0.0)
446
462
  confidence = result.get("confidence", 0.0)
463
+
464
+ if not self._opts.enable_diarization:
465
+ # Fast path — no diarization, so we don't need to walk the
466
+ # utterance array. Use the original behavior: top-level
467
+ # ``result.text`` (cumulative across the current utterance)
468
+ # and ``utterances[0]`` for the active utterance's definite
469
+ # flag and timing. ``speaker_id`` stays None so this path
470
+ # is indistinguishable from the pre-diarization plugin.
471
+ self._emit_segment(
472
+ text=text,
473
+ definite=utterances[0].get("definite", False),
474
+ start_time=utterances[0].get("start_time", 0.0),
475
+ end_time=utterances[0].get("end_time", 0.0),
476
+ confidence=confidence,
477
+ speaker_id=None,
478
+ metadata=None,
479
+ )
480
+ return
481
+
482
+ # Diarization path — ``utterances`` is a cumulative list of
483
+ # completed sentence segments (see the Volcengine ASR docs).
484
+ # In streaming mode each response usually carries one active
485
+ # utterance, but end-of-stream flushes can return several at
486
+ # once. Emit each segment as its own event so
487
+ # ``MultiSpeakerAdapter`` (or any other downstream consumer) can
488
+ # apply per-speaker logic on a per-segment basis.
489
+ for utterance in utterances:
490
+ utt_text = utterance.get("text") or ""
491
+ if not utt_text:
492
+ continue
493
+ speaker_id, metadata = self._extract_diarization(utterance)
494
+ self._emit_segment(
495
+ text=utt_text,
496
+ definite=utterance.get("definite", False),
497
+ start_time=utterance.get("start_time", 0.0),
498
+ end_time=utterance.get("end_time", 0.0),
499
+ confidence=confidence,
500
+ speaker_id=speaker_id,
501
+ metadata=metadata,
502
+ )
503
+
504
+ def _extract_diarization(
505
+ self, utterance: dict
506
+ ) -> tuple[str | None, dict | None]:
507
+ """Pull ``speaker_id`` from ``additions.speaker`` and, when the
508
+ segment is definite, the ``gender`` tag for ``metadata``. Per
509
+ the Volcengine docs, ``additions.gender`` is only present on
510
+ definite utterances — interim updates legitimately omit it."""
511
+ additions = utterance.get("additions") or {}
512
+ raw_speaker = additions.get("speaker")
513
+ speaker_id = f"speaker_{raw_speaker}" if raw_speaker is not None else None
514
+
515
+ metadata: dict | None = None
516
+ if utterance.get("definite", False):
517
+ raw_gender = additions.get("gender")
518
+ if raw_gender is not None:
519
+ metadata = {"gender": str(raw_gender)}
520
+
521
+ return speaker_id, metadata
522
+
523
+ def _emit_segment(
524
+ self,
525
+ *,
526
+ text: str,
527
+ definite: bool,
528
+ start_time: float,
529
+ end_time: float,
530
+ confidence: float,
531
+ speaker_id: str | None,
532
+ metadata: dict | None,
533
+ ) -> None:
534
+ """Run the START/INTERIM/FINAL/END state machine for a single
535
+ segment. The plugin itself does not do main-speaker detection
536
+ or background suppression — that's the framework's
537
+ ``MultiSpeakerAdapter``'s job. We just emit per-segment events
538
+ with ``speaker_id`` set so the adapter can apply its policy."""
539
+ language = self._opts.language
540
+
541
+ def _make_alternative() -> stt.SpeechData:
542
+ return stt.SpeechData(
543
+ language=language,
544
+ text=text,
545
+ start_time=start_time,
546
+ end_time=end_time,
547
+ confidence=confidence,
548
+ speaker_id=speaker_id,
549
+ metadata=metadata,
550
+ )
551
+
447
552
  if not definite and not self._speaking:
448
553
  self._speaking = True
449
554
  self._event_ch.send_nowait(
450
555
  stt.SpeechEvent(type=stt.SpeechEventType.START_OF_SPEECH)
451
556
  )
452
557
  logger.info("transcription start")
453
- if text:
454
- alternatives = [
455
- stt.SpeechData(
456
- language=language,
457
- text=text,
458
- start_time=start_time,
459
- end_time=end_time,
460
- confidence=confidence,
461
- )
462
- ]
463
- self._event_ch.send_nowait(
464
- stt.SpeechEvent(
465
- type=stt.SpeechEventType.INTERIM_TRANSCRIPT,
466
- request_id=self._request_id,
467
- alternatives=alternatives,
468
- )
558
+ self._event_ch.send_nowait(
559
+ stt.SpeechEvent(
560
+ type=stt.SpeechEventType.INTERIM_TRANSCRIPT,
561
+ request_id=self._request_id,
562
+ alternatives=[_make_alternative()],
469
563
  )
564
+ )
470
565
  elif not definite and self._speaking:
471
- alternatives = [
472
- stt.SpeechData(
473
- text=text,
474
- start_time=start_time,
475
- end_time=end_time,
476
- confidence=confidence,
477
- language=language,
478
- )
479
- ]
480
566
  self._event_ch.send_nowait(
481
567
  stt.SpeechEvent(
482
568
  type=stt.SpeechEventType.INTERIM_TRANSCRIPT,
483
569
  request_id=self._request_id,
484
- alternatives=alternatives,
570
+ alternatives=[_make_alternative()],
485
571
  )
486
572
  )
487
573
  elif definite and self._speaking:
488
- alternatives = [
489
- stt.SpeechData(
490
- text=text,
491
- start_time=start_time,
492
- end_time=end_time,
493
- confidence=confidence,
494
- language=language,
495
- )
496
- ]
497
574
  self._event_ch.send_nowait(
498
575
  stt.SpeechEvent(
499
576
  type=stt.SpeechEventType.FINAL_TRANSCRIPT,
500
577
  request_id=self._request_id,
501
- alternatives=alternatives,
578
+ alternatives=[_make_alternative()],
502
579
  )
503
580
  )
504
581
  self._event_ch.send_nowait(
@@ -509,6 +586,29 @@ class SpeechStream(stt.SpeechStream):
509
586
  )
510
587
  self._speaking = False
511
588
  logger.info("transcription end", extra={"text": text})
589
+ elif definite and not self._speaking:
590
+ # The ASR flushed a finished segment without any preceding
591
+ # interim update in this session — typical for short
592
+ # snippets or for segments that arrived in the same batch
593
+ # as a previous FINAL. Emit a complete cycle so the agent
594
+ # still sees the text instead of dropping it.
595
+ self._event_ch.send_nowait(
596
+ stt.SpeechEvent(type=stt.SpeechEventType.START_OF_SPEECH)
597
+ )
598
+ self._event_ch.send_nowait(
599
+ stt.SpeechEvent(
600
+ type=stt.SpeechEventType.FINAL_TRANSCRIPT,
601
+ request_id=self._request_id,
602
+ alternatives=[_make_alternative()],
603
+ )
604
+ )
605
+ self._event_ch.send_nowait(
606
+ stt.SpeechEvent(
607
+ type=stt.SpeechEventType.END_OF_SPEECH,
608
+ request_id=self._request_id,
609
+ )
610
+ )
611
+ logger.info("transcription end (flushed)", extra={"text": text})
512
612
 
513
613
 
514
614
  def parse_response(res):
@@ -0,0 +1 @@
1
+ __version__ = "1.3.13"
@@ -3,9 +3,7 @@ name = "livekit-plugins-volcenginee"
3
3
  dynamic = ["version"]
4
4
  description = "LiveKit Agent Plugins for Volcengine"
5
5
  readme = "README.md"
6
- authors = [
7
- { name = "linsanzhu", email = "2287225730@qq.com" }
8
- ]
6
+ authors = [{ name = "linsanzhu", email = "2287225730@qq.com" }]
9
7
  keywords = ["webrtc", "realtime", "audio", "video", "livekit"]
10
8
  requires-python = ">=3.10"
11
9
  dependencies = [
@@ -13,7 +11,7 @@ dependencies = [
13
11
  "osc-data~=0.3.1",
14
12
  "pydantic",
15
13
  "numpy",
16
- "livekit-agents~=1.5.13",
14
+ "livekit-agents>=1.5.13",
17
15
  ]
18
16
 
19
17
  classifiers = [
@@ -1 +0,0 @@
1
- __version__ = "1.3.12"