Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
816f892
Add livekit-plugins-funasr: pyproject.toml
LauraGPT Jun 21, 2026
e6eb02a
Add livekit-plugins-funasr: README.md
LauraGPT Jun 21, 2026
b93bd6d
Add livekit-plugins-funasr: __init__.py
LauraGPT Jun 21, 2026
ba6a1cb
Add livekit-plugins-funasr: stt.py
LauraGPT Jun 21, 2026
df3adcc
Add livekit-plugins-funasr: version.py
LauraGPT Jun 21, 2026
6cf385d
Add livekit-plugins-funasr: log.py
LauraGPT Jun 21, 2026
be69daf
Register livekit-plugins-funasr workspace source
LauraGPT Jun 21, 2026
576bf71
Fix CI: ruff format, type annotations, LanguageCode for SpeechData
LauraGPT Jun 21, 2026
6a8949f
Add py.typed marker so the package is type-checkable
LauraGPT Jun 21, 2026
f6163a7
Address review: real model name, drop nospeech as language, thread-sa…
LauraGPT Jun 21, 2026
161ab6c
Address FunASR STT review feedback
LauraGPT Jun 30, 2026
eeacb61
fix(funasr): complete plugin packaging
LauraGPT Jul 13, 2026
ed0dae1
Align FunASR plugin version with agents extra
LauraGPT Jul 18, 2026
63e4ab8
Fix LiveKit CI dependency locks after upstream merge
LauraGPT Jul 22, 2026
bf71537
Fix FunASR model loading lifecycle
LauraGPT Aug 3, 2026
59c4f5c
Keep FunASR inference serialized after cancellation
LauraGPT Aug 3, 2026
7732eeb
chore(deps): lock FunASR plugin dependencies
LauraGPT Sep 3, 2026
727f524
fix(funasr): snapshot request options and redact model logs
LauraGPT Sep 4, 2026
df6df9a
fix(funasr): align dependency and telemetry contracts
LauraGPT Sep 4, 2026
48e340e
fix(funasr): align plugin with LiveKit 1.8.0
LauraGPT Sep 5, 2026
3d9808e
fix(funasr): redact inference errors and model telemetry
LauraGPT Sep 6, 2026
67c9489
fix(funasr): preserve model identity across private telemetry
LauraGPT Sep 6, 2026
45925ac
fix: resolve current STT metadata for each user-turn span
LauraGPT Sep 6, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions livekit-agents/livekit/agents/stt/multi_speaker_adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from .stt import STT, RecognizeStream, SpeechData, SpeechEvent, SpeechEventType

if TYPE_CHECKING:
from ..llm.chat_context import MetricsMetadata
from ..voice.events import ConversationItemAddedEvent


Expand Down Expand Up @@ -79,6 +80,10 @@ def model(self) -> str:
def provider(self) -> str:
return self._stt.provider

@property
def metrics_metadata(self) -> MetricsMetadata:
return self._stt.metrics_metadata

def _update_session_keyterms(self, keyterms: list[str]) -> None:
self._stt._update_session_keyterms(keyterms)

Expand Down
5 changes: 5 additions & 0 deletions livekit-agents/livekit/agents/stt/stream_adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from .stt import STT, RecognizeStream, SpeechEvent, SpeechEventType, STTCapabilities

if TYPE_CHECKING:
from ..llm.chat_context import MetricsMetadata
from ..voice.events import ConversationItemAddedEvent

# already a retry mechanism in STT.recognize, don't retry in stream adapter
Expand Down Expand Up @@ -48,6 +49,10 @@ def model(self) -> str:
def provider(self) -> str:
return self._stt.provider

@property
def metrics_metadata(self) -> MetricsMetadata:
return self._stt.metrics_metadata

def _update_session_keyterms(self, keyterms: list[str]) -> None:
self._stt._update_session_keyterms(keyterms)

Expand Down
13 changes: 4 additions & 9 deletions livekit-agents/livekit/agents/stt/stt.py
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,7 @@ def provider(self) -> str:

@property
def metrics_metadata(self) -> MetricsMetadata:
"""Metadata used to label turn metrics emitted for this STT instance."""
"""Metadata used to label metrics, traces, and error events for this STT instance."""
return {"model_name": self.model, "model_provider": self.provider}

@property
Expand Down Expand Up @@ -235,10 +235,7 @@ async def recognize(
label=self._label,
audio_duration=calculate_audio_duration(buffer),
streamed=False,
metadata=Metadata(
model_name=self.model,
model_provider=self.provider,
),
metadata=Metadata(**self.metrics_metadata),
)
self.emit("metrics_collected", stt_metrics)
return event
Expand Down Expand Up @@ -455,7 +452,7 @@ def _report_connection_acquired(self, acquire_time: float, connection_reused: bo
streamed=True,
acquire_time=acquire_time,
connection_reused=connection_reused,
metadata=Metadata(model_name=self._stt.model, model_provider=self._stt.provider),
metadata=Metadata(**self._stt.metrics_metadata),
),
)

Expand Down Expand Up @@ -532,9 +529,7 @@ async def _metrics_monitor_task(self, event_aiter: AsyncIterable[SpeechEvent]) -
input_tokens=ev.recognition_usage.input_tokens,
output_tokens=ev.recognition_usage.output_tokens,
streamed=True,
metadata=Metadata(
model_name=self._stt.model, model_provider=self._stt.provider
),
metadata=Metadata(**self._stt.metrics_metadata),
)

self._stt.emit("metrics_collected", stt_metrics)
Expand Down
19 changes: 15 additions & 4 deletions livekit-agents/livekit/agents/voice/agent_activity.py
Original file line number Diff line number Diff line change
Expand Up @@ -748,10 +748,15 @@ def _update_models(
self._agent._stt = new_stt
resolved_stt = self.stt
if self._audio_recognition is not None:
stt_metadata = (
Metadata(**resolved_stt.metrics_metadata)
if isinstance(resolved_stt, stt.STT)
else Metadata()
)
self._audio_recognition._update_stt(
self._agent.stt_node if resolved_stt else None,
model=resolved_stt.model if isinstance(resolved_stt, stt.STT) else None,
provider=resolved_stt.provider if isinstance(resolved_stt, stt.STT) else None,
model=stt_metadata.model_name,
provider=stt_metadata.model_provider,
aligned_transcript=bool(resolved_stt.capabilities.aligned_transcript)
if isinstance(resolved_stt, stt.STT)
else False,
Expand Down Expand Up @@ -1160,6 +1165,7 @@ async def _start_session(self, *, reuse_resources: _ReusableResources | None = N
wired_vad = self.vad
if wired_vad is not None and self.using_default_vad and self._rt_turn_detection_enabled:
wired_vad = None
stt_metadata = Metadata(**self.stt.metrics_metadata) if self.stt else Metadata()
self._audio_recognition = AudioRecognition(
self._session,
hooks=self,
Expand All @@ -1168,8 +1174,13 @@ async def _start_session(self, *, reuse_resources: _ReusableResources | None = N
interruption_detection=self._interruption_detector,
endpointing=create_endpointing(self.endpointing_opts),
turn_detection=self._turn_detection,
stt_model=self.stt.model if self.stt else None,
stt_provider=self.stt.provider if self.stt else None,
stt_model=stt_metadata.model_name,
stt_provider=stt_metadata.model_provider,
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.
stt_metadata=lambda: (
Metadata(**current_stt.metrics_metadata)
if (current_stt := self.stt) is not None
else Metadata()
),
stt_aligned_transcript=bool(self.stt.capabilities.aligned_transcript)
if self.stt
else False,
Expand Down
4 changes: 3 additions & 1 deletion livekit-agents/livekit/agents/voice/amd/detector.py
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,7 @@ def __init__(
self._stt.model,
EVALUATED_STT_MODELS,
model_kind="stt",
display_model=self._stt.metrics_metadata.get("model_name"),
)

self._setup_task: asyncio.Task[None] | None = None
Expand Down Expand Up @@ -630,6 +631,7 @@ def _warn_if_not_evaluated(
evaluated_models: set[str],
*,
model_kind: str,
display_model: NotGivenOr[str | None] = NOT_GIVEN,
) -> None:
if not model:
return
Expand All @@ -643,5 +645,5 @@ def _warn_if_not_evaluated(
"%s model %s hasn't been evaluated with our benchmark, it might not be compatible "
"with amd. Set `suppress_compatibility_warning=True` to silence this warning.",
model_kind,
model,
model if not is_given(display_model) else display_model or "unknown",
)
18 changes: 13 additions & 5 deletions livekit-agents/livekit/agents/voice/audio_recognition.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
)
from ..language import LanguageCode
from ..log import logger
from ..metrics.base import Metadata
from ..stt import SpeechEvent
from ..telemetry import trace_types, tracer
from ..types import NOT_GIVEN, NotGivenOr
Expand Down Expand Up @@ -249,6 +250,7 @@ def __init__(
turn_detection: TurnDetectionMode | None,
stt_model: str | None = None,
stt_provider: str | None = None,
stt_metadata: Callable[[], Metadata] | None = None,
stt_aligned_transcript: bool = False,
) -> None:
self._session = session
Expand All @@ -264,6 +266,7 @@ def __init__(
self._vad = vad
self._stt_model = stt_model
self._stt_provider = stt_provider
self._stt_metadata = stt_metadata
self._stt_aligned_transcript = stt_aligned_transcript
self._turn_detection_mode = turn_detection if isinstance(turn_detection, str) else None
self._vad_base_turn_detection = self._turn_detection_mode in ("vad", None)
Expand Down Expand Up @@ -1944,14 +1947,19 @@ def _ensure_user_turn_span(self, start_time: float | None = None) -> trace.Span:
if (room_io := self._session._room_io) and room_io.linked_participant:
_set_participant_attributes(self._user_turn_span, room_io.linked_participant)

# add STT model/provider attributes
if self._stt_model:
# Resolve at span creation: fallback adapters can switch without an STT hot swap.
metadata = (
self._stt_metadata()
if self._stt_metadata is not None
else Metadata(model_name=self._stt_model, model_provider=self._stt_provider)
)
if metadata.model_name:
self._user_turn_span.set_attribute(
trace_types.ATTR_GEN_AI_REQUEST_MODEL, self._stt_model
trace_types.ATTR_GEN_AI_REQUEST_MODEL, metadata.model_name
)
if self._stt_provider:
if metadata.model_provider:
self._user_turn_span.set_attribute(
trace_types.ATTR_GEN_AI_PROVIDER_NAME, self._stt_provider
trace_types.ATTR_GEN_AI_PROVIDER_NAME, metadata.model_provider
)

return self._user_turn_span
Expand Down
5 changes: 4 additions & 1 deletion livekit-agents/livekit/agents/voice/events.py
Original file line number Diff line number Diff line change
Expand Up @@ -552,7 +552,10 @@ class ErrorEvent(BaseModel):

@field_serializer("source")
def _serialize_source(self, source: Any) -> Any:
if isinstance(source, LLM | STT | TTS | RealtimeModel | AdaptiveInterruptionDetector):
if isinstance(source, STT):
metadata = source.metrics_metadata
return {"model": metadata.get("model_name"), "provider": metadata.get("model_provider")}
if isinstance(source, LLM | TTS | RealtimeModel | AdaptiveInterruptionDetector):
return {"model": source.model, "provider": source.provider}
if isinstance(source, BaseModel):
return source.model_dump()
Expand Down
1 change: 1 addition & 0 deletions livekit-agents/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ deepgram = ["livekit-plugins-deepgram>=1.8.0"]
elevenlabs = ["livekit-plugins-elevenlabs>=1.8.0"]
fal = ["livekit-plugins-fal>=1.8.0"]
fishaudio = ["livekit-plugins-fishaudio>=1.8.0"]
funasr = ["livekit-plugins-funasr>=1.8.0"]
fireworksai = ["livekit-plugins-fireworksai>=1.8.0"]
gladia = ["livekit-plugins-gladia>=1.8.0"]
gnani = ["livekit-plugins-gnani>=1.8.0"]
Expand Down
25 changes: 25 additions & 0 deletions livekit-plugins/livekit-plugins-funasr/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# LiveKit Plugins FunASR

Agent Framework plugin for local speech-to-text with [FunASR](https://github.com/modelscope/FunASR) models such as [SenseVoice](https://github.com/FunAudioLLM/SenseVoice).

SenseVoice is an open-source, fully-local, non-autoregressive multilingual ASR model (Chinese, Cantonese, English, Japanese, Korean and more) with leading Chinese accuracy and fast inference. The model runs locally, so no API key is required.

## Installation

```bash
pip install livekit-plugins-funasr
```

## Usage

```python
from livekit.plugins import funasr

stt = funasr.STT(model="iic/SenseVoiceSmall")
```

The first run downloads the model from ModelScope/Hugging Face. Use `language=None` (default) for automatic language detection, or set e.g. `language="zh"`.

CPU inference works with the default installation. For GPU inference, install a
PyTorch and torchaudio build that matches your CUDA runtime, then pass
`device="cuda"`.
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Copyright 2024 LiveKit, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""FunASR plugin for LiveKit Agents.

Local, fully-offline multilingual speech-to-text using FunASR models such as
SenseVoice (Chinese, Cantonese, English, Japanese, Korean and more).
See https://github.com/modelscope/FunASR for more information.
"""

from .stt import _DEFAULT_MODEL, FunASRSTT, FunASRSTT as STT, _load_model
from .version import __version__

__all__ = ["FunASRSTT", "STT", "__version__"]

from livekit.agents import Plugin

from .log import logger


class FunASRPlugin(Plugin):
"""Register the FunASR integration and its model-download hook."""

def __init__(self) -> None:
"""Create the LiveKit plugin registration."""
super().__init__(__name__, __version__, __package__, logger)

def download_files(self) -> None:
"""Download the default SenseVoice model for offline agent startup."""
_load_model(_DEFAULT_MODEL, "cpu")


Plugin.register_plugin(FunASRPlugin())
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import logging

logger = logging.getLogger("livekit.plugins.funasr")
Empty file.
Loading