Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
15 changes: 15 additions & 0 deletions livekit-plugins/livekit-plugins-meta/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Meta plugin for LiveKit Agents

Support for speech-to-text with [Meta](https://dev.meta.ai/)'s Muse Voice Transcribe.

See the [Meta integration docs](https://docs.livekit.io/agents/integrations/stt/meta/) for more information.

## Installation

```bash
pip install livekit-plugins-meta
```

## Pre-requisites

You'll need an API key from the Meta Model API. It can be set as an environment variable: `META_API_KEY`
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# Copyright 2026 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.

"""Meta plugin for LiveKit Agents

Speech-to-text with Muse Voice Transcribe.
See https://dev.meta.ai/docs/speech-to-text for more information.
"""

from .models import MuseEncoding, MuseMode, MuseModels, MusePartialMode
from .stt import STT, SpeechStream
from .version import __version__

__all__ = [
"STT",
"MuseEncoding",
"MuseMode",
"MuseModels",
"MusePartialMode",
"SpeechStream",
"__version__",
]

from livekit.agents import Plugin

from .log import logger


class MetaPlugin(Plugin):
def __init__(self) -> None:
super().__init__(__name__, __version__, __package__, logger)


Plugin.register_plugin(MetaPlugin())

# Cleanup docs of unexported modules
_module = dir()
NOT_IN_ALL = [m for m in _module if m not in __all__]

__pdoc__ = {}

for n in NOT_IN_ALL:
__pdoc__[n] = False
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
from __future__ import annotations

import time
from typing import TYPE_CHECKING, Generic, TypeVar

if TYPE_CHECKING:
from collections.abc import Callable

T = TypeVar("T")


class PeriodicCollector(Generic[T]):
def __init__(self, callback: Callable[[T], None], *, duration: float) -> None:
"""Accumulate values and hand the total to `callback` every `duration` seconds.

Args:
callback: Called with the accumulated total when `duration` elapses.
duration: Seconds between callback invocations.
"""
self._duration = duration
self._callback = callback
self._last_flush_time = time.monotonic()
self._total: T | None = None

def push(self, value: T) -> None:
"""Add a value to the accumulator."""
if self._total is None:
self._total = value
else:
self._total += value # type: ignore[operator]
if time.monotonic() - self._last_flush_time >= self._duration:
self.flush()

def flush(self) -> None:
"""Report the current total, if any, and start a new period."""
if self._total is not None:
self._callback(self._total)
self._total = None
self._last_flush_time = time.monotonic()
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import logging

logger = logging.getLogger("livekit.plugins.meta")
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
from __future__ import annotations

from typing import Literal

from livekit.agents.language import LanguageCode

from .log import logger

MuseModels = Literal["muse-voice-transcribe-1.0"]

MuseEncoding = Literal["PCM_24KHZ", "PCM_16KHZ"]
"""Signed 16-bit little-endian mono PCM, at 24 kHz or 16 kHz."""

SAMPLE_RATES: dict[str, int] = {"PCM_24KHZ": 24000, "PCM_16KHZ": 16000}

MuseMode = Literal["PUSH_TO_TALK", "ENDPOINTING", "DIARIZATION"]
"""PUSH_TO_TALK: the caller delimits the turn. ENDPOINTING: the model detects turn
boundaries, one turn per speech segment. DIARIZATION: adds speaker attribution."""

MusePartialMode = Literal["CUMULATIVE", "DELTA"]
"""CUMULATIVE: every ``transcript`` event carries the whole current hypothesis and
replaces the previous one. DELTA: per-chunk text, for the file endpoint's SSE mode."""

# The 25 languages Meta lists as evaluated for muse-voice-transcribe-1.0. Muse takes
# `languageBias` as English language *names*, not BCP-47 codes, so this is keyed by the
# base code LanguageCode.language produces ("en-US" and "cmn-Hans-CN" both reduce here).
SUPPORTED_LANGUAGES: frozenset[str] = frozenset(
{
"ar",
"bn",
"de",
"en",
"es",
"fr",
"he",
"hi",
"id",
"it",
"ja",
"kn",
"ko",
"mr",
"ms",
"nl",
"pl",
"pt",
"ta",
"te",
"th",
"tl",
"tr",
"vi",
"zh",
}
)

# LanguageCode.to_language_name() covers 23 of the 25 (lowercased); these two it does
# not: it calls zh "chinese" where Meta's list says "Mandarin Chinese", and it has no
# name for Filipino, which is Tagalog on Meta's side.
_LANGUAGE_NAME_OVERRIDES: dict[str, str] = {"zh": "Mandarin Chinese", "fil": "Tagalog"}


def to_language_bias(languages: list[str]) -> list[str]:
"""Map BCP-47 codes onto the English language names Muse's `languageBias` expects.

Codes Muse does not list are dropped with a warning rather than forwarded: the
handshake is rejected wholesale on an unknown bias entry, which would take down
recognition entirely instead of merely losing the hint.
"""
names: list[str] = []
for lang in languages:
code = LanguageCode(lang)
base = code.language
name = _LANGUAGE_NAME_OVERRIDES.get(base)
if name is None and base in SUPPORTED_LANGUAGES:
resolved = code.to_language_name()
name = resolved.title() if resolved else None
if name is None:
logger.warning(
"language is not supported by Muse Voice Transcribe, dropping from languageBias",
extra={"language": lang},
)
Comment thread
CHVSAnirudh marked this conversation as resolved.
continue
if name not in names:
names.append(name)
return names
Empty file.
Loading