Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
import weakref
from collections.abc import Sequence
from dataclasses import dataclass
from typing import Any
from typing import Any, Literal

import aiohttp

Expand All @@ -45,6 +45,8 @@
from .log import logger
from .models import V2Models

FluxRedaction = Literal["numbers", "aggressive_numbers"]


@dataclass
class STTOptions:
Expand All @@ -57,6 +59,9 @@ class STTOptions:
eot_threshold: NotGivenOr[float] = NOT_GIVEN
eot_timeout_ms: NotGivenOr[int] = NOT_GIVEN
mip_opt_out: bool = False
numerals: bool = False
profanity_filter: bool = False
redact: NotGivenOr[FluxRedaction] = NOT_GIVEN
tags: NotGivenOr[list[str]] = NOT_GIVEN
language_hint: NotGivenOr[list[str]] = NOT_GIVEN

Expand All @@ -77,6 +82,9 @@ def __init__(
http_session: aiohttp.ClientSession | None = None,
base_url: str = "wss://api.deepgram.com/v2/listen",
mip_opt_out: bool = False,
numerals: bool = False,
profanity_filter: bool = False,
redact: NotGivenOr[FluxRedaction] = NOT_GIVEN,
# deprecated
keyterms: NotGivenOr[list[str]] = NOT_GIVEN,
) -> None:
Expand All @@ -95,6 +103,9 @@ def __init__(
http_session: Optional aiohttp ClientSession to use for requests.
base_url: The base URL for Deepgram API. Defaults to "https://api.deepgram.com/v1/listen".
mip_opt_out: Whether to take part in the model improvement program
numerals: Whether to convert spoken numbers into numerical formats. Applied at connection time; Flux does not support toggling it mid-stream. Defaults to False.
profanity_filter: Whether to filter profanity from the transcription. Applied at connection time. Defaults to False.
redact: Redact numbers from the transcription, "numbers" or "aggressive_numbers". Flux does not support entity redaction (pci, pii, ...). Applied at connection time. Defaults to NOT_GIVEN.

Raises:
ValueError: If no API key is provided or found in environment variables.
Expand Down Expand Up @@ -145,6 +156,9 @@ def __init__(
if is_given(keyterm)
else [],
mip_opt_out=mip_opt_out,
numerals=numerals,
profanity_filter=profanity_filter,
redact=redact,
tags=_validate_tags(tags) if is_given(tags) else [],
language_hint=language_hint if is_given(language_hint) else [],
eager_eot_threshold=eager_eot_threshold,
Expand Down Expand Up @@ -210,6 +224,9 @@ def update_options(
eot_timeout_ms: NotGivenOr[int] = NOT_GIVEN,
keyterm: NotGivenOr[str | list[str]] = NOT_GIVEN,
mip_opt_out: NotGivenOr[bool] = NOT_GIVEN,
numerals: NotGivenOr[bool] = NOT_GIVEN,
profanity_filter: NotGivenOr[bool] = NOT_GIVEN,
redact: NotGivenOr[FluxRedaction] = NOT_GIVEN,
tags: NotGivenOr[list[str]] = NOT_GIVEN,
language_hint: NotGivenOr[list[str]] = NOT_GIVEN,
endpoint_url: NotGivenOr[str] = NOT_GIVEN,
Expand Down Expand Up @@ -247,6 +264,12 @@ def update_options(
self._opts.keyterm = keyterm
if is_given(mip_opt_out):
self._opts.mip_opt_out = mip_opt_out
if is_given(numerals):
self._opts.numerals = numerals
if is_given(profanity_filter):
self._opts.profanity_filter = profanity_filter
if is_given(redact):
self._opts.redact = redact
if is_given(tags):
self._opts.tags = _validate_tags(tags)
if is_given(language_hint):
Expand All @@ -269,6 +292,9 @@ def update_options(
eot_timeout_ms=eot_timeout_ms,
keyterm=keyterm,
mip_opt_out=mip_opt_out,
numerals=numerals,
profanity_filter=profanity_filter,
redact=redact,
endpoint_url=endpoint_url,
tags=tags,
language_hint=language_hint,
Expand Down Expand Up @@ -327,6 +353,9 @@ def update_options(
eot_timeout_ms: NotGivenOr[int] = NOT_GIVEN,
keyterm: NotGivenOr[str | list[str]] = NOT_GIVEN,
mip_opt_out: NotGivenOr[bool] = NOT_GIVEN,
numerals: NotGivenOr[bool] = NOT_GIVEN,
profanity_filter: NotGivenOr[bool] = NOT_GIVEN,
redact: NotGivenOr[FluxRedaction] = NOT_GIVEN,
tags: NotGivenOr[list[str]] = NOT_GIVEN,
language_hint: NotGivenOr[list[str]] = NOT_GIVEN,
endpoint_url: NotGivenOr[str] = NOT_GIVEN,
Expand All @@ -351,6 +380,12 @@ def update_options(
self._opts.keyterm = keyterm
if is_given(mip_opt_out):
self._opts.mip_opt_out = mip_opt_out
if is_given(numerals):
self._opts.numerals = numerals
if is_given(profanity_filter):
self._opts.profanity_filter = profanity_filter
if is_given(redact):
self._opts.redact = redact
if is_given(tags):
self._opts.tags = _validate_tags(tags)
if is_given(language_hint):
Expand All @@ -360,9 +395,21 @@ def update_options(
if is_given(eager_eot_threshold):
self._opts.eager_eot_threshold = eager_eot_threshold

# these only take effect on a fresh connection
# these only take effect on a fresh connection: Flux does not support
# toggling numerals, profanity_filter, or redact through Configure
# https://developers.deepgram.com/docs/numerals
needs_reconnect = any(
is_given(opt) for opt in (model, sample_rate, mip_opt_out, tags, endpoint_url)
is_given(opt)
for opt in (
model,
sample_rate,
mip_opt_out,
numerals,
profanity_filter,
redact,
tags,
endpoint_url,
)
)
if needs_reconnect:
# reconnect carries the latest options
Expand Down Expand Up @@ -538,7 +585,7 @@ async def recv_task(ws: aiohttp.ClientWebSocketResponse) -> None:
if ws is not None:
await ws.close()

async def _connect_ws(self) -> aiohttp.ClientWebSocketResponse:
def _live_config(self) -> dict[str, Any]:
live_config: dict[str, Any] = {
"model": self._opts.model,
"sample_rate": self._opts.sample_rate,
Expand All @@ -564,6 +611,20 @@ async def _connect_ws(self) -> aiohttp.ClientWebSocketResponse:
if self._opts.language_hint:
live_config["language_hint"] = self._opts.language_hint

if self._opts.numerals:
live_config["numerals"] = self._opts.numerals

if self._opts.profanity_filter:
live_config["profanity_filter"] = self._opts.profanity_filter

if is_given(self._opts.redact):
live_config["redact"] = self._opts.redact

return live_config

async def _connect_ws(self) -> aiohttp.ClientWebSocketResponse:
live_config = self._live_config()

try:
ws = await asyncio.wait_for(
self._session.ws_connect(
Expand Down
35 changes: 35 additions & 0 deletions tests/test_plugin_deepgram_stt.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ def _make_flux_stream(*, ws=None, **opts_kwargs):
eager_eot_threshold=opts_kwargs.get("eager_eot_threshold", NOT_GIVEN),
eot_timeout_ms=opts_kwargs.get("eot_timeout_ms", NOT_GIVEN),
language_hint=opts_kwargs.get("language_hint", []),
numerals=opts_kwargs.get("numerals", False),
profanity_filter=opts_kwargs.get("profanity_filter", False),
redact=opts_kwargs.get("redact", NOT_GIVEN),
)
opts.keyterm = opts_kwargs.get("keyterm", [])
stream = SimpleNamespace(
Expand Down Expand Up @@ -105,6 +108,38 @@ async def test_flux_reconnect_fields_skip_inband_configure():
assert ws.sent == []


@pytest.mark.parametrize(
("field", "value"),
[("numerals", True), ("profanity_filter", True), ("redact", "numbers")],
)
async def test_flux_connection_time_fields_trigger_reconnect_not_configure(field, value):
from livekit.plugins.deepgram.stt_v2 import SpeechStreamv2

ws = _FakeWS()
stream = _make_flux_stream(ws=ws)
SpeechStreamv2.update_options(stream, **{field: value})

# Flux can't toggle these via Configure, only at connection time
assert getattr(stream._opts, field) == value
assert stream._reconnect_event.is_set()
assert stream._reconfigure_atask is None
assert ws.sent == []


async def test_flux_connection_config_includes_formatting_fields():
from livekit.plugins.deepgram.stt_v2 import SpeechStreamv2

config = SpeechStreamv2._live_config(
_make_flux_stream(numerals=True, profanity_filter=True, redact="aggressive_numbers")
)
assert config["numerals"] is True
assert config["profanity_filter"] is True
assert config["redact"] == "aggressive_numbers"

default_config = SpeechStreamv2._live_config(_make_flux_stream())
assert not {"numerals", "profanity_filter", "redact"} & default_config.keys()


async def test_flux_configure_sends_only_changed_fields():
from livekit.plugins.deepgram.stt_v2 import SpeechStreamv2

Expand Down