diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 86b213c..5c8d751 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,9 +2,9 @@ name: CI on: push: - branches: [main, develop] + branches: [master, develop] pull_request: - branches: [main] + branches: [master] jobs: lint-and-test: @@ -28,12 +28,6 @@ jobs: path: ~/.cache/pip key: ${{ runner.os }}-pip-${{ matrix.python-version }}-${{ hashFiles('pyproject.toml') }} - - name: Install build tools - run: pip install hatchling grpcio-tools - - - name: Generate gRPC stubs - run: python hatch_build.py - - name: Install package with dev dependencies run: pip install -e ".[dev]" @@ -62,7 +56,7 @@ jobs: python-version: "3.11" - name: Install build tools - run: pip install build hatchling grpcio-tools + run: pip install build hatchling - name: Build wheel and sdist run: python -m build diff --git a/.gitignore b/.gitignore index fcedf93..97262a1 100644 --- a/.gitignore +++ b/.gitignore @@ -16,9 +16,6 @@ venv/ env/ .env -# Generated gRPC stubs (regenerate with: make proto) -livekit/plugins/techmo/_proto/techmo/ - # IDEs .idea/ .vscode/ diff --git a/CHANGELOG.md b/CHANGELOG.md index f3feca0..a9a0a6d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,26 @@ All notable changes to this project will be documented here. Format follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). +## [Unreleased] + +### Fixed +- Fresh installs failing on import with `VersionError` (protobuf gencode newer than runtime): + the build hook generated stubs with whatever `grpcio-tools` was newest, independent of the + protobuf runtime in the environment +- Editable installs (`pip install -e .`) shadowing `livekit.agents`: the empty + `livekit/__init__.py` and `livekit/plugins/__init__.py` turned the shared namespace into a + regular package; they are removed + +### Changed +- gRPC stubs now come from the `techmo-asr-api` package (1.1.4) instead of being generated + from `proto/` at build time; `hatch_build.py`, `proto/` and the `grpcio-tools` build + dependency are removed, and `--no-build-isolation` is no longer needed +- Dependencies: `protobuf>=5.29.0` and `grpcio>=1.70.0` (required by the `techmo-asr-api` + stubs), `livekit-agents<2.0` +- CI now runs on pushes and pull requests to `master` (it targeted a non-existent `main`) +- README requirements aligned with the package metadata +- Lint, format and mypy issues fixed so the CI passes; mypy no longer pins `python_version` + ## [0.2.0] - 2026-09-30 ### Added diff --git a/Makefile b/Makefile index e166fd7..b8ee3d0 100644 --- a/Makefile +++ b/Makefile @@ -1,14 +1,6 @@ -.PHONY: proto install install-dev lint format typecheck test clean +.PHONY: install install-dev lint format typecheck test clean -PROTO_DIR := proto -OUTPUT_DIR := livekit/plugins/techmo/_proto - -# Generate gRPC/protobuf Python stubs from .proto files -proto: - pip install grpcio-tools --quiet - python hatch_build.py - -# Install the package in editable/dev mode (also generates protos) +# Install the package in editable/dev mode install-dev: pip install -e ".[dev]" @@ -28,7 +20,6 @@ test: pytest tests/ -v clean: - rm -rf $(OUTPUT_DIR)/techmo rm -rf build dist *.egg-info find . -type d -name __pycache__ -exec rm -rf {} + 2>/dev/null || true find . -name "*.pyc" -delete diff --git a/README.md b/README.md index dda7728..524eb0e 100644 --- a/README.md +++ b/README.md @@ -11,42 +11,34 @@ - TLS and mutual-TLS support - Multiple language groups / model selection - MRCP timeout controls (no-input, recognition, speech-complete, speech-incomplete) -- Compatible with LiveKit Agents >= 1.5 +- Compatible with LiveKit Agents 1.x ## Requirements - Python >= 3.10 -- `livekit-agents >= 1.5` -- `grpcio >= 1.63` -- `protobuf >= 5.0` +- `livekit-agents >= 1.0, < 2.0` (tested with 1.5.1 and 1.8.3) +- `grpcio >= 1.70` +- `protobuf >= 5.29` +- [`techmo-asr-api`](https://github.com/techmo-pl/asr-api-python) 1.1.4 (gRPC stubs; installed automatically) - Access to a running **Techmo ASR** gRPC server ## Installation -### From source - ```bash -git clone https://github.com/techmo-pl/livekit-plugins-techmo -cd livekit-plugins-techmo - -# Install build tools -pip install grpcio-tools hatchling - -# Install the plugin (stubs are generated at build time) -pip install --no-build-isolation . +pip install "livekit-plugins-techmo @ git+https://github.com/techmo-pl/livekit-plugins-techmo.git@" ``` -> **Note:** The `--no-build-isolation` flag is required because the build hook generates -> gRPC Python stubs from the `.proto` files in `proto/` at install time. -> The stubs are placed in `livekit/plugins/techmo/_proto/`. - -To regenerate stubs manually after changing `.proto` files: +or from a clone: ```bash -pip install grpcio-tools -python hatch_build.py +git clone https://github.com/techmo-pl/livekit-plugins-techmo +cd livekit-plugins-techmo +pip install . ``` +The gRPC stubs come from the [`techmo-asr-api`](https://github.com/techmo-pl/asr-api-python) +package (a dependency), so nothing is generated at install time. + ## Quick Start ```python @@ -166,9 +158,6 @@ LIVEKIT_LOG_LEVEL=DEBUG python my_agent.py dev ## Development ```bash -# Generate gRPC stubs -python hatch_build.py - # Run linter ruff check . @@ -184,8 +173,8 @@ TECHMO_ASR_ADDRESS=localhost:5555 pytest tests/test_integration.py -v ## API Version -This plugin uses the **Techmo ASR v1p1** gRPC API. The `.proto` definition is located at -`proto/techmo/asr/api/v1p1/asr.proto`. +This plugin uses the **Techmo ASR v1p1** gRPC API (`asr_api.v1p1` from +[`techmo-asr-api`](https://github.com/techmo-pl/asr-api-python), which also holds the `.proto` definitions). ## License diff --git a/examples/basic_stt.py b/examples/basic_stt.py index 1e0810e..473dfe5 100644 --- a/examples/basic_stt.py +++ b/examples/basic_stt.py @@ -20,9 +20,9 @@ JobContext, WorkerOptions, cli, - llm, ) from livekit.agents.pipeline import VoicePipelineAgent + from livekit.plugins.techmo import STT logger = logging.getLogger("techmo-example") @@ -35,7 +35,7 @@ async def entrypoint(ctx: JobContext) -> None: # service_address can also be read from TECHMO_ASR_ADDRESS env var stt_engine = STT( service_address=os.environ.get("TECHMO_ASR_ADDRESS", "localhost:5555"), - language_group="pl", # Polish language group — adjust as needed + language_group="pl", # Polish language group — adjust as needed interim_results=True, sample_rate=16000, ) diff --git a/examples/streaming_stt_standalone.py b/examples/streaming_stt_standalone.py index 586f0b6..9257f45 100644 --- a/examples/streaming_stt_standalone.py +++ b/examples/streaming_stt_standalone.py @@ -12,7 +12,6 @@ import asyncio import logging import os -import struct import sys import wave @@ -34,11 +33,10 @@ def read_wav_pcm(path: str) -> tuple[bytes, int]: async def transcribe_file(wav_path: str) -> None: try: from livekit import rtc + from livekit.plugins.techmo.stt import STT except ImportError as e: - raise SystemExit( - "Install dependencies first: pip install livekit-plugins-techmo livekit-rtc" - ) from e + raise SystemExit("Install dependencies first: pip install livekit-plugins-techmo livekit-rtc") from e address = os.environ.get("TECHMO_ASR_ADDRESS", "localhost:5555") pcm, sample_rate = read_wav_pcm(wav_path) diff --git a/hatch_build.py b/hatch_build.py deleted file mode 100644 index 2cea38e..0000000 --- a/hatch_build.py +++ /dev/null @@ -1,113 +0,0 @@ -"""Hatchling build hook: generates gRPC/protobuf stubs from .proto files.""" - -from __future__ import annotations - -import os -import sys -from pathlib import Path - -from hatchling.builders.hooks.plugin.interface import BuildHookInterface - - -PROTO_DIR = Path(__file__).parent / "proto" -OUTPUT_DIR = Path(__file__).parent / "livekit" / "plugins" / "techmo" / "_proto" - -PROTO_FILES = [ - "techmo/api/status.proto", - "techmo/asr/api/v1p1/asr.proto", -] - - -class CustomBuildHook(BuildHookInterface): - PLUGIN_NAME = "custom" - - def initialize(self, version: str, build_data: dict) -> None: - _generate_protos() - - -def _generate_protos() -> None: - try: - from grpc_tools import protoc - except ImportError: - print( - "WARNING: grpcio-tools not found. Skipping proto generation. " - "Run 'pip install grpcio-tools && python -m grpc_tools.protoc ...' manually.", - file=sys.stderr, - ) - return - - OUTPUT_DIR.mkdir(parents=True, exist_ok=True) - - # Write __init__.py so the package is importable - init_file = OUTPUT_DIR / "__init__.py" - if not init_file.exists(): - init_file.write_text("# Auto-generated gRPC stubs package\n") - - # Collect include paths: our proto dir + grpc_tools bundled protos - import grpc_tools - - grpc_tools_proto_dir = Path(grpc_tools.__file__).parent / "_proto" - include_paths = [str(PROTO_DIR), str(grpc_tools_proto_dir)] - include_args = [f"-I{p}" for p in include_paths] - - for proto_file in PROTO_FILES: - args = [ - "grpc_tools.protoc", - *include_args, - f"--python_out={OUTPUT_DIR}", - f"--grpc_python_out={OUTPUT_DIR}", - f"--pyi_out={OUTPUT_DIR}", - proto_file, - ] - ret = protoc.main(args) - if ret != 0: - raise RuntimeError(f"protoc failed for {proto_file} (exit code {ret})") - - # Ensure every subdirectory under OUTPUT_DIR is a proper Python package - _ensure_init_files(OUTPUT_DIR) - - # Fix absolute imports in all generated files (protoc generates absolute imports) - _fix_proto_imports(OUTPUT_DIR) - - print(f"gRPC stubs generated in {OUTPUT_DIR}") - - -def _ensure_init_files(output_dir: Path) -> None: - """Create __init__.py in every subdirectory so they are importable packages.""" - for dirpath in output_dir.rglob("*"): - if dirpath.is_dir(): - init = dirpath / "__init__.py" - if not init.exists(): - init.write_text("") - - -def _fix_proto_imports(output_dir: Path) -> None: - """Replace absolute package imports with relative ones in all generated pb2 files. - - protoc generates absolute imports like ``from techmo.x import y_pb2`` which - break when the stubs are nested under ``_proto/``. We compute the correct - relative prefix for each file based on its depth under output_dir. - """ - for pb_file in output_dir.glob("**/*.py"): - content = pb_file.read_text() - if "from techmo." not in content: - continue - - # Compute how many package levels separate this file from output_dir. - # Each level = one dot in the relative import prefix. - depth = len(pb_file.relative_to(output_dir).parts) - 1 # -1 for the file itself - prefix = "." * depth # e.g. depth=4 → "...." for _proto/techmo/asr/api/v1p1/ - - # Replace every absolute "from techmo. import" with a relative one. - # e.g. depth=4, "from techmo.api import" → "from ....api import" - import re - content = re.sub( - r"from techmo((?:\.\w+)*) import", - lambda m: f"from {prefix}{m.group(1).lstrip('.')} import", - content, - ) - pb_file.write_text(content) - - -if __name__ == "__main__": - _generate_protos() diff --git a/livekit/__init__.py b/livekit/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/livekit/plugins/__init__.py b/livekit/plugins/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/livekit/plugins/techmo/__init__.py b/livekit/plugins/techmo/__init__.py index 59c068f..294de82 100644 --- a/livekit/plugins/techmo/__init__.py +++ b/livekit/plugins/techmo/__init__.py @@ -3,7 +3,7 @@ from livekit.agents import Plugin from .log import logger -from .stt import STT, STTOptions, SpeechStream +from .stt import STT, SpeechStream, STTOptions from .version import __version__ __all__ = [ diff --git a/livekit/plugins/techmo/_proto/__init__.py b/livekit/plugins/techmo/_proto/__init__.py deleted file mode 100644 index 955987a..0000000 --- a/livekit/plugins/techmo/_proto/__init__.py +++ /dev/null @@ -1,55 +0,0 @@ -"""Auto-generated gRPC/protobuf stubs for Techmo ASR v1p1. - -This package is generated by the build system. Run: - - python hatch_build.py - -or - - make proto - -to regenerate from the .proto files in the proto/ directory. -""" - -try: - # Protoc places generated files in a nested path matching the proto package. - # After generation into _proto/, the files will be at: - # _proto/techmo/asr/api/v1p1/asr_pb2.py - # _proto/techmo/asr/api/v1p1/asr_pb2_grpc.py - from .techmo.asr.api.v1p1.asr_pb2 import ( # type: ignore - Audio, - AudioConfig, - ResultConfig, - SpeechRecognitionConfig, - SpeechRecognitionResult, - StreamingConfig, - StreamingRecognizeRequest, - StreamingRecognizeRequestConfig, - StreamingRecognizeRequestControlMessage, - StreamingRecognizeRequestData, - StreamingRecognizeResponse, - StreamingRecognizeResult, - ) - from .techmo.asr.api.v1p1.asr_pb2_grpc import AsrStub # type: ignore - - __all__ = [ - "Audio", - "AudioConfig", - "AsrStub", - "ResultConfig", - "SpeechRecognitionConfig", - "SpeechRecognitionResult", - "StreamingConfig", - "StreamingRecognizeRequest", - "StreamingRecognizeRequestConfig", - "StreamingRecognizeRequestControlMessage", - "StreamingRecognizeRequestData", - "StreamingRecognizeResponse", - "StreamingRecognizeResult", - ] - -except ImportError as exc: - raise ImportError( - "Techmo ASR gRPC stubs have not been generated yet.\n" - "Run 'python hatch_build.py' or 'make proto' from the project root." - ) from exc diff --git a/livekit/plugins/techmo/stt.py b/livekit/plugins/techmo/stt.py index 55251cf..7c05d95 100644 --- a/livekit/plugins/techmo/stt.py +++ b/livekit/plugins/techmo/stt.py @@ -3,29 +3,25 @@ from __future__ import annotations import asyncio -import dataclasses import os -import time +from collections.abc import AsyncIterator, Callable from dataclasses import dataclass -from typing import AsyncIterator +from typing import Any import grpc import grpc.aio - from livekit import rtc from livekit.agents import stt, utils from livekit.agents.types import DEFAULT_API_CONNECT_OPTIONS, NOT_GIVEN, APIConnectOptions, NotGivenOr from .log import logger -from .version import __version__ try: - from . import _proto # type: ignore[attr-defined] + from asr_api import v1p1 as _proto except ImportError as e: raise ImportError( - "Techmo ASR gRPC stubs not found. " - "Run 'pip install grpcio-tools && python hatch_build.py' to generate them, " - "or reinstall the package: 'pip install livekit-plugins-techmo'" + "Techmo ASR gRPC stubs (techmo-asr-api) not found. Reinstall the plugin, or install them with: " + "pip install 'techmo-asr-api @ git+https://github.com/techmo-pl/asr-api-python.git@1.1.4'" ) from e @@ -149,8 +145,7 @@ def __init__( service_address = os.environ.get("TECHMO_ASR_ADDRESS") if not service_address: raise ValueError( - "service_address is required. " - "Pass it directly or set the TECHMO_ASR_ADDRESS environment variable." + "service_address is required. Pass it directly or set the TECHMO_ASR_ADDRESS environment variable." ) self._opts = STTOptions( @@ -185,7 +180,7 @@ def _make_channel(self) -> grpc.aio.Channel: return grpc.aio.secure_channel(opts.service_address, credentials) return grpc.aio.insecure_channel(opts.service_address) - async def _recognize_impl( + async def _recognize_impl( # type: ignore[override] self, buffer: utils.AudioBuffer, *, @@ -193,9 +188,7 @@ async def _recognize_impl( ) -> stt.SpeechEvent: """Perform batch (non-streaming) recognition on a complete audio buffer.""" config = _build_config(self._opts) - audio_bytes = b"".join( - frame.data.tobytes() for frame in buffer - ) + audio_bytes = b"".join(frame.data.tobytes() for frame in buffer) # type: ignore[union-attr] async with self._make_channel() as channel: stub = _proto.AsrStub(channel) @@ -226,7 +219,7 @@ async def _requests() -> AsyncIterator[_proto.StreamingRecognizeRequest]: # typ return stt.SpeechEvent( type=stt.SpeechEventType.FINAL_TRANSCRIPT, - alternatives=alternatives or [stt.SpeechData(language="", text="")], + alternatives=alternatives or [stt.SpeechData(language="", text="")], # type: ignore[arg-type] recognition_usage=stt.RecognitionUsage(audio_duration=duration), ) @@ -236,7 +229,7 @@ def stream( language: NotGivenOr[str] = NOT_GIVEN, conn_options: APIConnectOptions = DEFAULT_API_CONNECT_OPTIONS, **kwargs: object, - ) -> "SpeechStream": + ) -> SpeechStream: """Return an async streaming speech recognition context.""" return SpeechStream(self, self._opts, conn_options=conn_options) @@ -274,9 +267,7 @@ async def _request_generator() -> AsyncIterator[_proto.StreamingRecognizeRequest ) ) - recv_task = asyncio.ensure_future( - self._recv_loop(stub, _request_generator, send_queue) - ) + recv_task = asyncio.ensure_future(self._recv_loop(stub, _request_generator, send_queue)) resampler: rtc.AudioResampler | None = None @@ -303,12 +294,12 @@ async def _request_generator() -> AsyncIterator[_proto.StreamingRecognizeRequest async def _recv_loop( self, - stub: object, - request_gen_factory: object, + stub: Any, + request_gen_factory: Callable[[], AsyncIterator[Any]], send_queue: asyncio.Queue[bytes | None], ) -> None: try: - async for response in stub.StreamingRecognize( # type: ignore[union-attr] + async for response in stub.StreamingRecognize( request_gen_factory(), timeout=self._opts.grpc_timeout, metadata=_build_metadata(self._opts), @@ -408,7 +399,7 @@ def _build_metadata(opts: STTOptions) -> tuple[tuple[str, str], ...] | None: return ((_SESSION_ID_METADATA_KEY, opts.session_id),) -def _build_config(opts: STTOptions) -> "_proto.StreamingRecognizeRequestConfig": # type: ignore[name-defined] +def _build_config(opts: STTOptions) -> _proto.StreamingRecognizeRequestConfig: # type: ignore[name-defined] """Build the initial gRPC config message from plugin options.""" speech_cfg_kwargs: dict[str, object] = { "enable_speech_recognition": True, @@ -460,7 +451,7 @@ def _parse_alternatives(result: object) -> list[stt.SpeechData]: if not text: continue confidence = alt.confidence if alt.HasField("confidence") else 0.0 - alts.append(stt.SpeechData(language="", text=text, confidence=confidence)) + alts.append(stt.SpeechData(language="", text=text, confidence=confidence)) # type: ignore[arg-type] return alts diff --git a/proto/google/rpc/status.proto b/proto/google/rpc/status.proto deleted file mode 100644 index 923e169..0000000 --- a/proto/google/rpc/status.proto +++ /dev/null @@ -1,49 +0,0 @@ -// Copyright 2022 Google LLC -// -// 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. - -syntax = "proto3"; - -package google.rpc; - -import "google/protobuf/any.proto"; - -option cc_enable_arenas = true; -option go_package = "google.golang.org/genproto/googleapis/rpc/status;status"; -option java_multiple_files = true; -option java_outer_classname = "StatusProto"; -option java_package = "com.google.rpc"; -option objc_class_prefix = "RPC"; - -// The `Status` type defines a logical error model that is suitable for -// different programming environments, including REST APIs and RPC APIs. It is -// used by [gRPC](https://github.com/grpc). Each `Status` message contains -// three pieces of data: error code, error message, and error details. -// -// You can find out more about this error model and how to work with it in the -// [API Design Guide](https://cloud.google.com/apis/design/errors). -message Status { - // The status code, which should be an enum value of - // [google.rpc.Code][google.rpc.Code]. - int32 code = 1; - - // A developer-facing error message, which should be in English. Any - // user-facing error message should be localized and sent in the - // [google.rpc.Status.details][google.rpc.Status.details] field, or localized - // by the client. - string message = 2; - - // A list of messages that carry the error details. There is a common set of - // message types for APIs to use. - repeated google.protobuf.Any details = 3; -} diff --git a/proto/techmo/api/status.proto b/proto/techmo/api/status.proto deleted file mode 100644 index 71d4ad2..0000000 --- a/proto/techmo/api/status.proto +++ /dev/null @@ -1,11 +0,0 @@ -syntax = "proto3"; -package techmo.api; - -// Status communicates the result of a recognition request. -// A non-zero code indicates an error. -message Status { - // Status code; 0 means success. - int32 code = 1; - // Human-readable description of the status. - string message = 2; -} diff --git a/proto/techmo/asr/api/v1p1/asr.proto b/proto/techmo/asr/api/v1p1/asr.proto deleted file mode 100644 index f2af15a..0000000 --- a/proto/techmo/asr/api/v1p1/asr.proto +++ /dev/null @@ -1,481 +0,0 @@ -// Copyright 2023 Techmo sp. z o.o. - -syntax = "proto3"; - -package techmo.asr.api.v1p1; - -import "google/protobuf/duration.proto"; -import "techmo/api/status.proto"; - - -// An automatic speech recognition (ASR) service providing a solution for -// speech-to-text conversion extended by the assessment of additional speech -// and speaker features. -service Asr { - // Perform bidirectional streaming recognition. - rpc StreamingRecognize(stream StreamingRecognizeRequest) - returns (stream StreamingRecognizeResponse) {} -} - -// A message streamed from the client through -// the [`StreamingRecognize`](#StreamingRecognize) method. -message StreamingRecognizeRequest { - oneof request_content { - // The immutable initial configuration of the request. - // Must be sent once in the request's first message. - StreamingRecognizeRequestConfig config = 1; - - // The message controlling the processing flow of the request. - // May be sent multiple times except in the request's first message. - StreamingRecognizeRequestControlMessage control_message = 2; - - // The data contents of the request itself. - // May be sent multiple times except in the request's first message. - StreamingRecognizeRequestData data = 3; - } -} - -// A message holding configuration of -// a [`StreamingRecognize`](#StreamingRecognize) request. -message StreamingRecognizeRequestConfig { - // Part of the configuration for the request's audio content. - AudioConfig audio_config = 1; - - // Part of the configuration for the request's result form. - ResultConfig result_config = 2; - - // Part of the configuration for the request's processing flow. - StreamingConfig streaming_config = 3; - - // Part of the configuration for speech recognition. - SpeechRecognitionConfig speech_recognition_config = 4; - - // Part of the configuration for age recognition. - AgeRecognitionConfig age_recognition_config = 5; - - // Part of the configuration for gender recognition. - GenderRecognitionConfig gender_recognition_config = 6; - - // Part of the configuration for language recognition. - LanguageRecognitionConfig language_recognition_config = 7; -} - -// Result configuration of -// a [`StreamingRecognize`](#StreamingRecognize) request. -message ResultConfig { - // The switch that toggles continuous recognition into single utterance mode. - // The service returns a final result for each end of utterance it detects in - // the audio, which may occur multiple times during a request. - // If enabled, the request terminates right after its first final result. - bool enable_single_utterance = 1; - - // The switch that allows interim results. - // If enabled, results containing tentative hypotheses may be returned in - // addition to final ones. - // The service should silently ignore this field if it is unsupported. - bool enable_interim_results = 2; - - // The switch to allow the service merging responses in the "hold response" - // state. - // If enabled and there is more than a single response held, the service does - // not return them in a batch. Instead, it tries to merge their results into - // a single response. - // The service should respond with the `INVALID_ARGUMENT` gRPC status code - // if the `recognition_alternatives_limit` field - // of the [`SpeechRecognitionConfig`](#SpeechRecognitionConfig) message is - // greater than 1. - // New in v1p1. - bool enable_held_responses_merging = 3; -} - -// Streaming configuration of -// a [`StreamingRecognize`](#StreamingRecognize) request. -message StreamingConfig { - reserved 1; // bool enable_single_utterance = 1; - - // The switch that enables manual control of the input timer. - // The timer imposes two constraints: one that finalizes recognition after - // a specified period unless speech is detected, and the other that limits - // the total time for an utterance. Manual control allows recognition to - // begin but delays enforcement of these constraints. The timer restarts - // after each detected end of utterance (each final result). - // If enabled, the timer does not start automatically. Instead, it can be - // initiated by sending - // a [`StreamingRecognizeRequestControlMessage`](#StreamingRecognizeRequestControlMessage) - // with the `start_input_timer` field set to `true` as needed. This should - // occur after the beginning of the request and be repeated after each final - // result. - bool enable_manual_input_timer = 2; - - // The switch to automatically set the service in the "hold response" state - // at the beginning of the request and after each final result. - // The "hold response" state means that the internal recognition process - // continues, but results are kept, not returned. When needed, the state can - // be toggled into the "give response" state by sending - // the [`StreamingRecognizeRequestControlMessage`](#StreamingRecognizeRequestControlMessage) - // message with the `give_response` field set to `true`. - // In the "give response" state the service responds as soon as it is ready. - // Any held responses may be returned in a batch or as a single merged - // response, provided that the `enable_held_responses_merging` field - // of the [`ResultConfig`](#ResultConfig) message is set to `true`. - // New in v1p1. - bool enable_auto_hold_response = 3; -} - -// Audio configuration of -// a [`StreamingRecognize`](#StreamingRecognize) request. -message AudioConfig { - // The possible audio encodings. - enum AudioEncoding { - // Unspecified audio encoding. - UNSPECIFIED = 0; - - // Linear pulse-code modulation of uncompressed 16-bit signed little-endian - // samples. - LINEAR16 = 1; - - // Free Lossless Audio Codec ([FLAC](https://wiki.xiph.org/FLAC)). - // The encoding requires only about half the bandwidth of `LINEAR16`. - // 16-bit and 24-bit samples. Not all fields in `STREAMINFO` are supported. - // When set, the service ignores the `sampling_rate_hz` field and detects - // the actual value from audio header instead. - FLAC = 2; - - // Ogg Encapsulated Opus Audio Codec ([OggOpus](https://wiki.xiph.org/OggOpus)). - // When set, the service ignores the `sampling_rate_hz` field and detects - // the actual value from audio header instead. - OGG_OPUS = 6; - - // MP3 (ISO/IEC 11172-3 and ISO/IEC 13818-3). - // Only constant bitrate. - // When set, the service ignores the `sampling_rate_hz` field and detects - // the actual value from audio header instead. - MP3 = 8; - } - - // The encoding of the audio data sent in the request. Single channel (mono) - // audio is assumed. - // The service should respond with the `INVALID_ARGUMENT` gRPC status code - // if the encoding is `UNSPECIFIED`. - // The service should respond with the `FAILED_PRECONDITION` gRPC status code - // if the encoding is not supported. - AudioEncoding encoding = 1; - - // The sampling rate of the audio data sent in the request. - // The service should silently ignore the field for encodings that are sent - // along wtih headers, and detect the value from them instead. - // The service should respond with the `INVALID_ARGUMENT` gRPC status code - // if the value is not greater than 0. - float sampling_rate_hz = 2; -} - -// Configuration of age recognition. -message AgeRecognitionConfig { - // The switch that enables age recognition for the request. - // If disabled or unspecified, the related results are excluded. - // The service responds with the `FAILED_PRECONDITION` gRPC status code - // if requested but not enabled. - bool enable_age_recognition = 1; -} - -// Configuration of gender recognition. -message GenderRecognitionConfig { - // The switch that enables gender recognition for the request. - // If disabled or unspecified, the related results are excluded. - // The service responds with the `FAILED_PRECONDITION` gRPC status code - // if requested but not enabled. - bool enable_gender_recognition = 1; -} - -// Configuration of language recognition. -message LanguageRecognitionConfig { - // The switch that enables language recognition for the request. - // If disabled or unspecified, the related results are excluded. - // The service responds with the `FAILED_PRECONDITION` gRPC status code - // if requested but not enabled. - bool enable_language_recognition = 1; -} - -// Configuration for speech recognition. -message SpeechRecognitionConfig { - // The switch that enables speech recognition for the request. - // If disabled or unspecified, the related results are excluded. - // The service responds with the `FAILED_PRECONDITION` gRPC status code - // if requested but not enabled. - bool enable_speech_recognition = 1; - - // The maximum number of alternative transcriptions allowed to be included - // per response. - // The actual count received can be less than the specified value and may - // also be equal to 0. If unspecified or 0, one alternative is allowed to be - // returned too. - uint32 recognition_alternatives_limit = 2; - - // The switch that enables additional time alignment of recognitions in word - // details. - // If enabled, the `words` field of - // a [`SpeechRecognitionAlternative`](#SpeechRecognitionAlternative) message - // includes a list of [`SpeechRecognitionWord`](#SpeechRecognitionWord) - // messages. Otherwise, it remains empty. - // The service responds with the `FAILED_PRECONDITION` gRPC status code - // if requested but not enabled. - bool enable_time_alignment = 3; - - // The name of a language group of models to be used. - // If left unspecified, it backs to the service's default group. - // The service responds with the `NOT_FOUND` gRPC status code - // if the name is not registered. - string language_group_name = 4; - - // The name of a model to be used. - // If left unspecified, it backs to the selected langugage group's default. - // The service responds with the `NOT_FOUND` gRPC status code - // if the name is not registered. - string model_name = 5; - - // Deprecated. - // The additional advanced service-dependend configuration for its speech - // recognizer. It may be silently ignored. - map config_fields = 6; -} - -// A message controlling the processing flow of -// a [`StreamingRecognize`](#StreamingRecognize) request. -message StreamingRecognizeRequestControlMessage { - reserved 2; - - oneof control_message_content - { - // The flag that starts the input timer on demand and resets after each final - // result. It is silently ignored if the manual input timer setting is - // disabled for the request. - bool start_input_timer = 1; - - // The flag to allow the service to return a response. - // After receiving this message, the service remains in the "give response" - // state. Ignored when the service is already in the "give response" state. - // Mutually exclusive with the `hold_response` field. - // New in v1p1. - bool give_response = 3; - - // The flag to forbid the service from returning a response. - // After receiving this message, the service remains in the "hold response" - // state. Ignored when the service is already in the "hold response" state. - // Mutually exclusive with the `give_response` field. - // New in v1p1. - bool hold_response = 4; - } -} - -// A message that carries data contents of -// a [`StreamingRecognizeRequest`](#StreamingRecognizeRequest) request. -message StreamingRecognizeRequestData { - // Part of the audio to perform recognition on. - Audio audio = 1; -} - -// Audio contents. -message Audio { - oneof audio_content { - // The audio data bytes. - bytes bytes = 1; - } -} - -// A message streamed from the service through -// the [`StreamingRecognize`](#StreamingRecognize) method. -message StreamingRecognizeResponse { - // The combined recognition results for another part of the audio. - StreamingRecognizeResult result = 1; - - // The cumulative duration of the processed audio during the request, - // not necessarily matching the actual length of the sent audio, mandatorily - // updated with each final result. - google.protobuf.Duration processed_audio_duration = 2; -} - -// Combined recognition result. -message StreamingRecognizeResult { - // The recognition process status. - // It may communicate warnings. In case of an error hindering recognition, - // all other message fields should be left unset. - techmo.api.Status error = 1; - - // The flag indicating whether the result is interim or final. - bool is_final = 2; - - // The anticipated causes for the service to finalize a result. - enum ResultFinalizationCause { - // The cause is not specified. - UNSPECIFIED = 0; - - // The speech recognition result is not empty and the end of utterance - // is detected. - SUCCESS = 1; - - // The speech recognition result is empty after the duration to expect - // a result is reached. - NO_INPUT_TIMEOUT = 2; - - // The speech recognition result is not empty after the utterance duration - // limit is reached. The returned speech recognition is incomplete and - // should be completed in the following result. - SUCCESS_MAXTIME = 3; - - // Unused. - PARTIAL_MATCH = 4; - - // The speech recognition result is empty after the utterance duration - // limit is reached. - NO_MATCH_MAXTIME = 5; - } - - // The field indicating the cause of result finalization. - // For interim results, the service should leave the field as `UNSPECIFIED`. - // For final results, the service must set the field to a value other than - // `UNSPECIFIED`. - ResultFinalizationCause result_finalization_cause = 3; - - // The speech recognition result for another part of the processed audio, - // new with each final result, updates with each interim one. - // To obtain a complete result for all processed audio, for each final result - // received, a client should pick one of the result's recognition alternatives - // and buffer it on its own. - // It must be omitted if speech recognition is disabled. - SpeechRecognitionResult speech_recognition_result = 4; - - // The current age recognition result for all processed audio, - // updated with each final result. - // It may be omitted in an interim result and must be omitted if age - // recognition is disabled. - AgeRecognitionResult age_recognition_result = 5; - - // The current gender recognition result for all processed audio, - // updated with each final result. - // It may be omitted in an interim result and must be omitted if gender - // recognition is disabled. - GenderRecognitionResult gender_recognition_result = 6; - - // The current language recognition result for all processed audio, - // updated with each final result. - // It may be omitted in an interim result and must be omitted if language - // recognition is disabled. - LanguageRecognitionResult language_recognition_result = 7; -} - -// A result of age recognition. -message AgeRecognitionResult { - // The recognition process status. - // It may communicate warnings. In case of an error hindering recognition, - // all other message fields should be left unset. - techmo.api.Status error = 1; - - // The confidence-ordered list of alternative recognition hypotheses. - repeated AgeRecognitionAlternative recognition_alternatives = 2; -} - -// An alternative hypothesis of age recognition. -message AgeRecognitionAlternative { - // The assumed age of the person speaking in the audio, in years. - // For a reliable value, assure that there is only one person speaking in - // the audio. - uint32 age = 1; - - // The confidence estimate, ranging from 0.0 to 1.0. - // Support for this feature is optional. - optional float confidence = 2; -} - -// A result of gender recognition. -message GenderRecognitionResult { - // The recognition process status. - // It may communicate warnings. In case of an error hindering recognition, - // all other message fields should be left unset. - techmo.api.Status error = 1; - - // The confidence-ordered list of alternative recognition hypotheses. - repeated GenderRecognitionAlternative recognition_alternatives = 2; -} - -// An alternative hypothesis of gender recognition. -message GenderRecognitionAlternative { - // The assumed gender of the person speaking in the audio. - // For a reliable value, assure that there is only one person speaking in - // the audio. - string gender = 1; - - // The confidence estimate, ranging from 0.0 to 1.0. - // Support for this feature is optional. - optional float confidence = 2; -} - -// A result of language recognition. -message LanguageRecognitionResult { - // The recognition process status. - // It may communicate warnings. In case of an error hindering recognition, - // all other message fields should be left unset. - techmo.api.Status error = 1; - - // The confidence-ordered list of alternative recognition hypotheses. - repeated LanguageRecognitionAlternative recognition_alternatives = 2; -} - -// An alternative hypothesis of language recognition. -message LanguageRecognitionAlternative { - // The language spoken in the audio, - // a [BCP-47](https://www.rfc-editor.org/rfc/bcp/bcp47.txt) tag. - string language = 1; - - // The confidence estimate, ranging from 0.0 to 1.0. - // Support for this feature is optional. - optional float confidence = 2; -} - -// A result of speech recognition. -message SpeechRecognitionResult { - // The recognition process status. - // It may communicate warnings. In case of an error hindering recognition, - // all other message fields should be left unset. - techmo.api.Status error = 1; - - // The confidence-ordered list of alternative recognition hypotheses. - repeated SpeechRecognitionAlternative recognition_alternatives = 2; - - // The actual name of the language group of the model, - // unrelated to the actual language spoken in the audio. - string language_group_name = 3; - - // The actual name of the model used to obtain the result. - string model_name = 4; -} - -// An alternative hypothesis of speech recognition. -message SpeechRecognitionAlternative { - // The transcript of the audio. - string transcript = 1; - - // The confidence estimate, ranging from 0.0 to 1.0. - // Support for this feature is optional. - optional float confidence = 2; - - // The details of the transcript's words. - // Empty unless `enable_time_alignment` is `true` in the request's - // [`SpeechRecognitionConfig`](#SpeechRecognitionConfig). - repeated SpeechRecognitionWord words = 3; -} - -// Details of a single word in speech recognition. -message SpeechRecognitionWord { - // The transcript of the word itself. - string transcript = 1; - - // The confidence estimate, ranging from 0.0 to 1.0. - // Support for this feature is optional. - optional float confidence = 2; - - // The start time of the word relative to the beginning of the entire audio. - google.protobuf.Duration start_time = 3; - - // The end time of the word relative to the beginning of the entire audio. - google.protobuf.Duration end_time = 4; -} diff --git a/pyproject.toml b/pyproject.toml index e65fc2e..a252a3d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [build-system] -requires = ["hatchling", "grpcio-tools>=1.63.0"] +requires = ["hatchling"] build-backend = "hatchling.build" [project] @@ -22,9 +22,12 @@ classifiers = [ "Topic :: Multimedia :: Sound/Audio :: Speech", ] dependencies = [ - "livekit-agents>=1.0", - "grpcio>=1.63.0", - "protobuf>=5.0.0", + "livekit-agents>=1.0,<2.0", + # techmo-asr-api stubs embed GRPC_GENERATED_VERSION 1.70.0 (older grpcio raises on import) + "grpcio>=1.70.0", + # techmo-asr-api ships stubs generated with protobuf 5.29 (older runtimes fail to import them) + "protobuf>=5.29.0", + "techmo-asr-api @ git+https://github.com/techmo-pl/asr-api-python.git@1.1.4", ] [project.urls] @@ -39,20 +42,17 @@ dev = [ "pytest-asyncio>=0.23", "ruff>=0.1.0", "mypy>=1.0", - "grpcio-tools>=1.63.0", ] -[tool.hatch.build.hooks.custom] -path = "hatch_build.py" +[tool.hatch.metadata] +# techmo-asr-api is not on PyPI yet, so it is referenced by git URL +allow-direct-references = true [tool.hatch.build.targets.wheel] packages = ["livekit"] -artifacts = [ - "livekit/plugins/techmo/_proto/techmo/", -] [tool.hatch.build.targets.sdist] -include = ["livekit", "proto", "hatch_build.py"] +include = ["livekit"] [tool.ruff] line-length = 120 @@ -61,8 +61,13 @@ target-version = "py310" [tool.ruff.lint] select = ["E", "F", "I", "UP"] +[tool.ruff.lint.isort] +# livekit/ is a namespace package shared with livekit-agents and livekit-rtc +known-third-party = ["livekit"] +known-first-party = ["livekit.plugins.techmo"] + [tool.mypy] -python_version = "3.10" +# no python_version pin: dependency stubs (e.g. numpy) use syntax of the running interpreter strict = false ignore_missing_imports = true diff --git a/tests/test_integration.py b/tests/test_integration.py index 94f812a..457cfb4 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -28,6 +28,7 @@ async def test_streaming_silence() -> None: """Send silence and expect either empty results or graceful handling.""" try: from livekit import rtc + from livekit.plugins.techmo.stt import STT except ImportError: pytest.skip("livekit-agents or gRPC stubs not installed") diff --git a/tests/test_stt.py b/tests/test_stt.py index 28ebfab..4ca1d7a 100644 --- a/tests/test_stt.py +++ b/tests/test_stt.py @@ -8,7 +8,7 @@ import asyncio import struct -from unittest.mock import AsyncMock, MagicMock, patch +from unittest.mock import MagicMock import pytest @@ -63,13 +63,13 @@ def test_sttoptions_custom() -> None: def test_stt_requires_address(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.delenv("TECHMO_ASR_ADDRESS", raising=False) with pytest.raises(ValueError, match="service_address"): - # Importing STT triggers proto import; skip if stubs not generated + # Importing STT triggers the techmo-asr-api import; skip if it is missing try: from livekit.plugins.techmo.stt import STT STT() except ImportError: - pytest.skip("gRPC stubs not generated") + pytest.skip("gRPC stubs (techmo-asr-api) not installed") def test_stt_address_from_env(monkeypatch: pytest.MonkeyPatch) -> None: @@ -80,12 +80,13 @@ def test_stt_address_from_env(monkeypatch: pytest.MonkeyPatch) -> None: stt = STT() assert stt._opts.service_address == "localhost:50051" except ImportError: - pytest.skip("gRPC stubs not generated") + pytest.skip("gRPC stubs (techmo-asr-api) not installed") def test_stt_capabilities() -> None: try: - from livekit.agents import stt as lk_stt + from livekit.agents import stt as lk_stt # noqa: F401 (availability check) + from livekit.plugins.techmo.stt import STT instance = STT(service_address="localhost:50051") @@ -110,7 +111,7 @@ def test_build_config_basic() -> None: assert cfg.result_config.enable_interim_results is True assert cfg.speech_recognition_config.enable_speech_recognition is True except ImportError: - pytest.skip("gRPC stubs not generated") + pytest.skip("gRPC stubs (techmo-asr-api) not installed") def test_build_config_with_model() -> None: @@ -128,7 +129,7 @@ def test_build_config_with_model() -> None: assert cfg.speech_recognition_config.model_name == "asr-pl-v2" assert cfg.speech_recognition_config.recognition_alternatives_limit == 3 except ImportError: - pytest.skip("gRPC stubs not generated") + pytest.skip("gRPC stubs (techmo-asr-api) not installed") # --------------------------------------------------------------------------- @@ -145,7 +146,7 @@ def test_proto_duration_to_seconds() -> None: dur.nanos = 500_000_000 assert _proto_duration_to_seconds(dur) == pytest.approx(2.5) except ImportError: - pytest.skip("gRPC stubs not generated") + pytest.skip("gRPC stubs (techmo-asr-api) not installed") # ---------------------------------------------------------------------------