I built a plug-in. I followed the doc structure. I...
# ask-ai
n
I built a plug-in. I followed the doc structure. I wanted to raise a LiveKit PR in order to get my plug-in listed officially as LiveKit's plug-in. What do I do?
t
Thanks for reaching out, Mythri. I'm an AI assistant and I'm here to help. Give me a moment while I research this topic.
Thanks — that’s great you’ve built a plugin and want it listed officially. I’ll walk you through the steps to submit a PR so your plugin can be considered for inclusion. Quick question before steps: Is your plugin written for Python or Node.js? The repo and exact file layout differ slightly between the two, so tell me which one and I’ll tailor the exact PR checklist. If you’re ready, here’s the general process and checklist you can follow now: 1) Pick the right repo • Python plugins: contribute to the livekit/agents repository. • Node.js plugins: contribute to the livekit/agents-js repository. 2) Ensure your plugin implements the standard interfaces and registration pattern • Implement the appropriate plugin base class and interfaces for your plugin type (e.g., STT, TTS, LLM). • Use the plugin registration pattern used by other plugins in the repo so it’s discoverable by the Agents framework. 3) Add the plugin as an optional dependency and include metadata • For Python: add your package as an optional dependency in the package/dependency files (e.g., pyproject.toml or equivalent used by the repo). • For Node.js: publish as an npm package and add it according to the repo’s plugin consumption pattern. • Include clear metadata such as package name, version, supported runtimes, and any configuration keys. 4) Documentation and examples (required) • Add a README describing what the plugin does, configuration and authentication steps, usage examples, and any setup required. • Provide a minimal code snippet showing how to register and use the plugin with an AgentSession (or equivalent) and how it fits in the STT→LLM→TTS pipeline if applicable. 5) Tests and validation • Include basic tests or an example app that demonstrates the plugin registration and a normal usage flow. • Make sure your plugin handles lifecycle events and streaming I/O the same way other plugins do (e.g., streaming audio chunks, error handling, and shutdown). 6) Coding standards and error handling • Follow the repository’s style and linting rules. • Ensure robust logging and clear error messages for misconfiguration or auth failures. 7) Prepare your PR • Fork the appropriate repo and create a feature branch. • Add your plugin files, tests, and docs. • Update any central index or dependency manifests the repo uses to list optional plugins. • Create a descriptive PR title and description that explains what the plugin does, how to configure it, and any relevant example usage or test instructions. 8) Submit the PR and include reviewers’ guidance • In the PR description, include: ◦ Language (Python / Node.js) ◦ Short summary and supported features (e.g., streaming STT, chunked TTS) ◦ Any external credentials or steps needed to test ◦ A pointer to example code or a small demo project • Ask maintainers to run any provided tests and point them to files showing the registration pattern. If you want, I can: • Review your plugin checklist or README before you open the PR. • Point out the exact files to modify in the correct repo once you tell me Python vs Node.js. • Provide a PR description template you can paste into GitHub. Which language is your plugin for, and would you like me to review your README or a short PR description draft? Sources: Large language models (LLM) overview | LiveKit Documentation | Agents (Python) | livekit/livekit_composite | DeepWiki | Models overview | LiveKit Documentation
n
Yes, sure, you can review my readme file, this is the link to my GitHub plugin. You can go through the entire code, the file structure, and everything : https://github.com/pinch-eng/livekit-plugins-pinch
t
Thanks — I can definitely review your README and the repo structure. I can’t fetch external URLs directly, so please paste the README contents (or the key files/folders you want reviewed) here. To make the review most useful, please include: • The full README.md • Any plugin registration or entry files (e.g., the file that implements the Plugin class or exports the package) • The package manifest (e.g.,
pyproject.toml
,
setup.py
, or
package.json
) • Any example usage snippet or demo script If you prefer, paste just the README and I’ll start there and request any additional files I need.
n
This is my pyproject.toml file : [build-system] requires = ["hatchling"] build-backend = "hatchling.build" [project] name = "livekit-plugins-pinch" version = "0.1.0" description = "LiveKit Agents plugin for Pinch real-time speech-to-speech translation" readme = "README.md" license = { text = "Apache-2.0" } authors = [{ name = "Pinch", email = "support@startpinch.com" }] keywords = [ "livekit", "agents", "translation", "speech-to-speech", "real-time", "pinch", ] classifiers = [ "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Topic :: Multimedia :: Sound/Audio :: Speech", "Topic :: Scientific/Engineering :: Artificial Intelligence", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", ] requires-python = ">=3.10" dependencies = [ "pinch-sdk>=0.1.1", "livekit>=0.12.0", "livekit-agents>=0.8.0", ] [project.urls] Homepage = "https://www.startpinch.com" Documentation = "https://www.startpinch.com/docs" Repository = "https://github.com/pinch-eng/livekit-plugins-pinch" "Bug Tracker" = "https://github.com/pinch-eng/livekit-plugins-pinch/issues" # --------------------------------------------------------------------------- # Hatchling build — namespace package configuration # # livekit/ and livekit/plugins/ are implicit namespace packages (PEP 420) # and intentionally contain no init.py. Hatchling includes the entire # livekit/ tree; pip will merge it with any other livekit.* packages already # installed in the same environment. # --------------------------------------------------------------------------- [tool.hatch.build.targets.wheel] packages = ["livekit"] [tool.hatch.build.targets.sdist] include = [ "livekit/", "examples/", "README.md", "pyproject.toml", ]
t
Thanks — I’ve got your
pyproject.toml
. This looks like a good start: package metadata, dependencies on
livekit
and
livekit-agents
, and clear project URLs. A few quick observations up front and then a short checklist of the next files I’ll need to do a full review. What I checked so far • Name, version, description, README and repo links are present. • Python compatibility and dependency pins exist for
pinch-sdk
,
livekit
, and
livekit-agents
. • Hatchling build config includes the
livekit/
tree so your package will install into the
livekit
namespace — that matches the common pattern for LiveKit plugins. Small issues and suggestions (actionable) • requires-python: the file currently shows
>=3.10
(HTML-escaped). Make sure it reads
>=3.10
in the actual file. • Consider tightening dependency version ranges if you want stricter compatibility guarantees (optional). • Ensure you include any runtime extras or optional dependencies if parts of your plugin are optional (e.g., extra dependencies for streaming or testing), using
optional-dependencies
or extras in the project table. Files I need to fully review your plugin and prepare a PR checklist Please paste the contents of these files (or the relevant excerpts) so I can review and give exact PR text and file-change recommendations: • README.md (full content) • The plugin implementation file(s) — the module that implements the Plugin class or registers the plugin (e.g., the file under
livekit/plugins/…
) • package entry points / registration code (if any) • Any example usage or demo script (shows how to register/use the plugin with an AgentSession) • Tests (if present) or an example test script • Any CI config (GitHub Actions) or linting configs (optional but helpful) • If Python: confirm whether you want this included as an optional dependency in the main
livekit/agents
project and, if so, whether you added it to any dependency manifest there (I can recommend exact edits once I see plugin code). What I’ll do once you paste those • Review README for clarity, required auth/config steps, and example usage; suggest tweaks and a PR-ready README. • Review plugin registration code and suggest the exact files and lines to add/update in the
livekit/agents
repo (e.g., dependency lists, index files). • Provide a PR checklist and a ready-to-paste PR description template that includes testing instructions and reviewer guidance. If you’d like, paste the README first and I’ll start there. If you prefer, paste multiple files at once — whatever is easiest.
n
livekit/plugins/pinch/__init__.py: """livekit-plugins-pinch — real-time speech-to-speech translation via Pinch.""" from .models import TranscriptEvent, TranslatorOptions from .translator import ( PinchAuthError, PinchError, PinchRateLimitError, PinchSessionError, Translator, ) from .version import version all = [ # Core classes "Translator", "TranslatorOptions", "TranscriptEvent", # Exceptions "PinchError", "PinchAuthError", "PinchRateLimitError", "PinchSessionError", # Version "__version__", ] livekit/plugins/pinch/log.py: import logging logger = logging.getLogger("livekit.plugins.pinch") livekit/plugins/pinch/models.py: from future import annotations from dataclasses import dataclass, field @dataclass class TranslatorOptions: """Configuration options for a Pinch real-time translation session. Attributes: source_language: BCP-47 language code for the input audio (e.g. ``"en-US"``). Pinch uses this as a hint to improve accuracy even when auto-detection is on. target_language: BCP-47 language code for the translated output (e.g. ``"es-ES"``). voice_type: Voice style for the synthesised translation. One of ``"clone"`` (default), ``"female"``, or ``"male"``. """ source_language: str target_language: str voice_type: str = field(default="clone") def __post_init__(self) -> None: allowed_voice_types = {"clone", "female", "male"} if self.voice_type not in allowed_voice_types: raise ValueError( f"voice_type must be one of {allowed_voice_types!r}, got {self.voice_type!r}." ) @dataclass class TranscriptEvent: """A transcript message emitted from the Pinch data channel. Both interim and final transcripts for the source language as well as the translated language are delivered through this object. Attributes: type: Message kind — either ``"original_transcript"`` (source language recognition) or ``"translated_transcript"`` (target language output). text: The recognised or translated text for this segment. is_final: ``True`` once Pinch has committed the segment and will not update it further. language_detected: BCP-47 code of the language Pinch detected in the audio (e.g. ``"en-US"``). timestamp: Unix timestamp (seconds, fractional) reported by Pinch for when the utterance ended. confidence: Raw confidence score returned by Pinch (0–1 range, ``0`` when not provided). """ type: str text: str is_final: bool language_detected: str timestamp: float confidence: float = field(default=0.0) @property def is_original(self) -> bool: """``True`` if this is a source-language transcript.""" return self.type == "original_transcript" @property def is_translated(self) -> bool: """``True`` if this is a translated-language transcript.""" return self.type == "translated_transcript" livekit/plugins/pinch/translator.py,
readme: # livekit-plugins-pinch Real-time voice translation for [LiveKit Agents](https://github.com/livekit/agents), powered by [Pinch](https://www.startpinch.com). --- ## What it does Drop this plugin into any LiveKit room and it will: - Translate spoken audio in real time from one language to another - Publish the translated audio back into the room as a separate track - Emit transcripts (original + translated) via a simple callback --- ## Installation
Copy code
bash
pip install livekit-plugins-pinch
--- ## Configuration You need a Pinch API key. Get one at the [developers portal](https://portal.startpinch.com/dashboard/developers). Set it in your environment:
Copy code
bash
export PINCH_API_KEY=pk_your_key_here
That's the only credential this plugin needs. Your LiveKit credentials stay in your own app as usual. --- ## Usage
Copy code
python
from livekit import rtc
from livekit.plugins.pinch import Translator, TranslatorOptions

async def entrypoint(ctx: JobContext):
    await ctx.connect()

    translator = Translator(
        options=TranslatorOptions(
            source_language="en-US",
            target_language="es-ES",
            voice_type="clone",  # "clone" | "female" | "male"
        )
    )

    @translator.on_transcript
    def on_transcript(event):
        if event.is_final:
            print(f"[{event.type}] {event.text}")

    await translator.start(ctx.room)
--- ## Voice types | Value | Description | |-------|-------------| |
clone
| Preserves the speaker's original voice identity (default) | |
female
| Standard female voice | |
male
| Standard male voice | --- ## Supported languages Full list of language codes: [supported languages](https://www.startpinch.com/docs/supported-languages) --- ## License Apache 2.0 — see [LICENSE](./LICENSE) for details.
t
Thanks — this is a great, clear start. I reviewed the files you pasted (pyproject.toml, package init, models, README) and below I’ve listed: (A) quick fixes and clarifications, (B) recommended README/UX improvements, (C) suggested code/package checks for PR readiness, and (D) a PR checklist and ready-to-paste PR description template. I also note a few files I still need to fully verify integration. A — Quick fixes and clarifications • In pyproject.toml,
requires-python
shows an HTML-escaped value (
>=3.10
). Make sure it reads
>=3.10
. • Confirm
tool.hatch.build.targets.wheel.packages = ["livekit"]
is intentional so your package is installed into the
livekit
namespace. That is the expected pattern for LiveKit plugins, but double-check installation locally to ensure the
livekit.plugins.pinch
modules import correctly. • Your package depends on
livekit-agents>=0.8.0
. Ensure the plugin API you use in translator.py is compatible with that minimum version (I’ll verify once I see translator.py). B — README: suggested edits for clarity and reviewer friendliness • Add a short “Prerequisites” section listing: ◦ LiveKit Agents version tested with (e.g., tested with livekit-agents X.Y.Z). ◦ Python versions you tested on (pyproject lists 3.10–3.12). • Make installation explicit about the package source: ◦ If you plan to publish to PyPI, keep
pip install livekit-plugins-pinch
. If not yet published, note how to install from GitHub (e.g., `pip install git+https://github.com/pinch-eng/livekit-plugins-pinch.git`). • Expand the Configuration section to include any environment variables, optional settings, and timeouts or region endpoints if applicable. • Expand Usage to show: ◦ Minimal example for registering the plugin with an Agents
AgentSession
or
JobContext
(you have a JobContext example; add one showing how this integrates into a full AgentSession lifecycle if applicable). ◦ How to unsubscribe or stop the translator (cleanup/lifecycle). • Add a “Testing” or “Local demo” section showing how maintainers can run a quick demo or unit tests. If testing requires Pinch credentials, explain how to run tests with mocks or set a CI secret. • Add a short “Security & Privacy” section if relevant (e.g., whether audio or transcripts are sent to Pinch and how you recommend treating sensitive data). • Add a one-line “Compatibility” note listing which LiveKit/Agents features your plugin integrates with (e.g., publishes an audio track, emits transcripts via callback). C — Code & packaging checks to do before opening the PR • translator.py — I still need this file to confirm: ◦ You implement the plugin registration pattern used by other LiveKit plugins (i.e., subclassing the
Plugin
base class or otherwise exposing a factory the agents project can import). ◦ The public API matches your README (methods like
start(ctx.room)
, callbacks named
@translator.on_transcript
, etc.). ◦ Proper exception classes (PinchAuthError, PinchRateLimitError, etc.) are defined and documented. ◦ Lifecycle handling: that `start()`/`stop()` manage resources and cleanly close the Pinch session and any data channels. ◦ Streaming behavior: handling of audio chunks and backpressure to avoid overrun. • Namespace packaging: ◦ Confirm the livekit/ tree layout in the wheel/sdist produces importable
livekit.plugins.pinch
modules. ◦ Ensure you do not include init.py in intermediate namespace packages if you want implicit namespace behavior — your comment indicates intentional implicit namespace packaging (PEP 420) which is fine. • Tests: ◦ Provide at least one unit test or example script that demonstrates registration and a mocked transcript emission. If Pinch requires an external network call, use a mock in tests so reviewers can validate quickly. • CI / formatting: ◦ If you have linters/formatters (black/ruff/isort) add a simple GitHub Actions job that runs tests and linting. This helps maintainers review and accept PRs faster. D — PR checklist & PR description template (ready to paste) Checklist (do these before opening PR) • [ ] Confirm
pyproject.toml
has
>=3.10
(not HTML-escaped). • [ ] Include
translator.py
and any registration/entry code under
livekit/plugins/pinch/
. • [ ] Add README items: Prereqs, usage example, testing instructions, compatibility. • [ ] Add at least one unit test or demo script (with mocks for Pinch) and CI that runs it. • [ ] Ensure package builds locally (
python -m build
or
hatch build
) and imports as
from livekit.plugins.pinch import Translator
. • [ ] Verify exception classes and public API surface match README examples. PR description template Title: Add Pinch real-time speech-to-speech translation plugin (livekit/plugins/pinch) Description: This PR adds a plugin that integrates Pinch real-time speech-to-speech translation with LiveKit Agents. What it does: • Translates incoming audio in real time (configurable source/target languages). • Publishes translated audio back into the room as a separate track. • Emits transcript events (original + translated) via a callback interface. Files added: • livekit/plugins/pinch/translator.py — core implementation and lifecycle handling • livekit/plugins/pinch/models.py — dataclasses for TranslatorOptions and TranscriptEvent • livekit/plugins/pinch/log.py — logger • livekit/plugins/pinch/*init*.py — exports and version • pyproject.toml, README.md, examples/, tests/ Testing & how to verify: 1. Install the package locally: ◦ pip install -e . 1. Run unit tests: ◦ pytest tests/ 1. Run the example demo (uses mocked Pinch credentials): ◦ python examples/demo.py ◦ Or, set
PINCH_API_KEY
and run the demo against the real Pinch service. Compatibility: • Tested with Python 3.10–3.12 • Tested with livekit-agents >=0.8.0 (please confirm exact tested Agents version) Notes for reviewers: • The plugin uses implicit namespace packaging so it installs within the
livekit
namespace. • Tests use mocks for external Pinch calls; real credentials are not required to run tests. • If maintainers prefer a separate top-level package instead of namespace packaging, I can update packaging to publish as an independent package that still exposes
livekit.plugins.pinch
. E — Next steps from me • Please paste translator.py (the full file) and any example/demo script and tests you have. I’ll: ◦ Verify the plugin registration pattern and suggest the exact diff to add in the livekit/agents repo (e.g., where to list the optional dependency or index the plugin). ◦ Suggest any small API or doc tweaks that will make the PR land faster. ◦ Draft the exact changes you should make to dependency manifests or index files in livekit/agents if you want me to. If you’d like, paste translator.py now and I’ll start the code-level review.
n
livekit/plugins/pinch/translator.py: """Pinch real-time speech-to-speech translation plugin for LiveKit Agents.""" from future import annotations import asyncio import contextlib import json import os import time from typing import Callable from livekit import rtc from .log import logger from .models import TranscriptEvent, TranslatorOptions _SAMPLE_RATE = 48_000 _NUM_CHANNELS = 1 _MAX_CONNECT_RETRIES = 3 _RETRY_BASE_DELAY = 1.0 _AGENT_TRACK_TIMEOUT = 30.0 class PinchError(Exception): """Base class for all Pinch plugin errors.""" class PinchAuthError(PinchError): """Raised when the API key is invalid or missing (HTTP 401).""" class PinchRateLimitError(PinchError): """Raised when the API rate limit is exceeded (HTTP 429).""" class PinchSessionError(PinchError): """Raised when session creation or room connection fails.""" class Translator: """Real-time speech-to-speech translation via the Pinch API. Usage:: from livekit.plugins.pinch import Translator, TranslatorOptions translator = Translator( options=TranslatorOptions( source_language="en-US", target_language="es-ES", voice_type="clone", ) ) @translator.on_transcript def handle(event): if event.is_translated and event.is_final: print(f"[{event.language_detected}] {event.text}") await translator.start(room) await translator.stop() Parameters ---------- api_key: Pinch API key. Falls back to the ``PINCH_API_KEY`` environment variable. options: Source/target languages and voice type. """ def __init__( self, *, options: TranslatorOptions, api_key: str | None = None, ) -> None: resolved_key = api_key or os.environ.get("PINCH_API_KEY") if not resolved_key: raise ValueError( "A Pinch API key is required. Supply it via the
api_key
" "argument or set the PINCH_API_KEY environment variable." ) self._api_key: str = resolved_key self._options: TranslatorOptions = options self._source_room: rtc.Room | None = None self._pinch_room: rtc.Room | None = None self._pinch_audio_source: rtc.AudioSource | None = None self._source_audio_source: rtc.AudioSource | None = None self._pinch_local_track: rtc.LocalAudioTrack | None = None self._source_local_track: rtc.LocalAudioTrack | None = None self._pinch_publication: rtc.LocalTrackPublication | None = None self._source_publication: rtc.LocalTrackPublication | None = None self._tasks: list[asyncio.Task] = [] self._transcript_callbacks: list[Callable[[TranscriptEvent], None]] = [] self._started = False self._stopped = False def on_transcript( self, callback: Callable[[TranscriptEvent], None] ) -> Callable[[TranscriptEvent], None]: """Register a transcript callback. Works as a decorator or direct call. Called for both ``original_transcript`` and ``translated_transcript`` events. """ self._transcript_callbacks.append(callback) return callback def remove_transcript_listener(self, callback: Callable[[TranscriptEvent], None]) -> None: """Remove a previously registered transcript callback.""" with contextlib.suppress(ValueError): self._transcript_callbacks.remove(callback) async def start(self, source_room: rtc.Room) -> None: """Start the Pinch translation pipeline. Raises ------ PinchAuthError If the API key is rejected. PinchRateLimitError If the API rate limit is exceeded. PinchSessionError If session creation or room connection fails after retries. RuntimeError If called after meth`stop`. """ if self._stopped: raise RuntimeError( "This Translator has already been stopped. " "Create a new instance to start a fresh session." ) if self._started: logger.warning("Translator.start() called more than once; ignoring.") return self._started = True self._source_room = source_room logger.info( "Pinch Translator starting src=%s tgt=%s voice=%s", self._options.source_language, self._options.target_language, self._options.voice_type, ) session = await self._create_session() pinch_url: str = session["url"] pinch_token: str = session["token"] pinch_room_name: str = session["room_name"] logger.info("Pinch session created room=%s", pinch_room_name) self._pinch_room = await self._connect_with_retry(pinch_url, pinch_token) logger.info("Connected to Pinch room room=%s", pinch_room_name) self._pinch_audio_source = rtc.AudioSource( sample_rate=_SAMPLE_RATE, num_channels=_NUM_CHANNELS ) self._pinch_local_track = rtc.LocalAudioTrack.create_audio_track( "pinch-input", self._pinch_audio_source ) self._pinch_publication = await self._pinch_room.local_participant.publish_track( self._pinch_local_track, rtc.TrackPublishOptions(source=rtc.TrackSource.SOURCE_MICROPHONE), ) self._source_audio_source = rtc.AudioSource( sample_rate=_SAMPLE_RATE, num_channels=_NUM_CHANNELS ) self._source_local_track = rtc.LocalAudioTrack.create_audio_track( "pinch-translated", self._source_audio_source ) self._source_publication = await self._source_room.local_participant.publish_track( self._source_local_track, rtc.TrackPublishOptions(source=rtc.TrackSource.SOURCE_MICROPHONE), ) self._pinch_room.on("data_received", self._on_pinch_data_received) self._tasks.append(asyncio.ensure_future(self._run_source_to_pinch_bridge())) self._tasks.append(asyncio.ensure_future(self._run_pinch_to_source_bridge())) logger.info("Pinch Translator started successfully.") async def stop(self) -> None: """Shut down the translation pipeline. Safe to call multiple times.""" if self._stopped: return self._stopped = True logger.info("Stopping Pinch Translator…") if self._pinch_room is not None: with contextlib.suppress(Exception): self._pinch_room.off("data_received", self._on_pinch_data_received) for task in self._tasks: task.cancel() if self._tasks: await asyncio.gather(*self._tasks, return_exceptions=True) self._tasks.clear() if self._pinch_room is not None: if self._pinch_publication is not None: with contextlib.suppress(Exception): await self._pinch_room.local_participant.unpublish_track( self._pinch_publication.sid ) with contextlib.suppress(Exception): await self._pinch_room.disconnect() self._pinch_room = None if self._source_room is not None and self._source_publication is not None: with contextlib.suppress(Exception): await self._source_room.local_participant.unpublish_track( self._source_publication.sid ) self._pinch_audio_source = None self._source_audio_source = None logger.info("Pinch Translator stopped.") async def _create_session(self) -> dict: """Create a Pinch translation session via the SDK.""" from pinch import PinchClient, SessionParams from pinch.errors import ( PinchAuthError as _SdkAuthError, PinchError as _SdkError, PinchRateLimitError as _SdkRateLimitError, ) params = SessionParams( source_language=self._options.source_language, target_language=self._options.target_language, voice_type=self._options.voice_type, ) try: client = PinchClient(api_key=self._api_key) session_info = await asyncio.to_thread(client.create_session, params) except _SdkAuthError as exc: raise PinchAuthError(str(exc)) from exc except _SdkRateLimitError as exc: raise PinchRateLimitError(str(exc)) from exc except _SdkError as exc: raise PinchSessionError(str(exc)) from exc return { "url": session_info.url, "token": session_info.token, "room_name": session_info.room_name, } async def _connect_with_retry(self, url: str, token: str) -> rtc.Room: last_exc: Exception | None = None for attempt in range(_MAX_CONNECT_RETRIES): room = rtc.Room() try: await room.connect(url, token) return room except Exception as exc: last_exc = exc if attempt == _MAX_CONNECT_RETRIES - 1: break delay = _RETRY_BASE_DELAY * (2**attempt) logger.warning( "Connection attempt %d/%d failed: %s — retrying in %.1fs", attempt + 1, _MAX_CONNECT_RETRIES, exc, delay, ) await asyncio.sleep(delay) raise PinchSessionError( f"Could not connect to the Pinch room after {_MAX_CONNECT_RETRIES} attempts." ) from last_exc
async def _run_source_to_pinch_bridge(self) -> None: if self._source_room is None: return streaming_tasks: list[asyncio.Task] = [] async def _stream_track(track: rtc.RemoteAudioTrack) -> None: try: audio_stream = rtc.AudioStream( track, sample_rate=_SAMPLE_RATE, num_channels=_NUM_CHANNELS ) async for event in audio_stream: if self._stopped: break if self._pinch_audio_source is not None: await self._pinch_audio_source.capture_frame(event.frame) except asyncio.CancelledError: pass except Exception as exc: if not self._stopped: logger.warning("source→Pinch bridge error on track %s: %s", track.sid, exc) def start_streaming(track: rtc.Track) -> None: if isinstance(track, rtc.RemoteAudioTrack): streaming_tasks.append(asyncio.ensure_future(_stream_track(track))) for participant in self._source_room.remote_participants.values(): for pub in participant.track_publications.values(): if pub.track is not None: start_streaming(pub.track) @self._source_room.on("track_subscribed") def _on_track_subscribed( track: rtc.Track, publication: rtc.RemoteTrackPublication, participant: rtc.RemoteParticipant, ) -> None: start_streaming(track) try: while not self._stopped: await asyncio.sleep(0.5) except asyncio.CancelledError: pass finally: self._source_room.off("track_subscribed", _on_track_subscribed) for t in streaming_tasks: t.cancel() if streaming_tasks: await asyncio.gather(*streaming_tasks, return_exceptions=True) async def _run_pinch_to_source_bridge(self) -> None: if self._pinch_room is None: return streaming_tasks: list[asyncio.Task] = [] async def _stream_track(track: rtc.RemoteAudioTrack) -> None: try: audio_stream = rtc.AudioStream( track, sample_rate=_SAMPLE_RATE, num_channels=_NUM_CHANNELS ) async for event in audio_stream: if self._stopped: break if self._source_audio_source is not None: await self._source_audio_source.capture_frame(event.frame) except asyncio.CancelledError: pass except Exception as exc: if not self._stopped: logger.warning("Pinch→source bridge error on track %s: %s", track.sid, exc) def start_streaming(track: rtc.Track) -> None: if isinstance(track, rtc.RemoteAudioTrack): logger.info( "Pinch agent published audio track sid=%s — forwarding to source room.", track.sid, ) streaming_tasks.append(asyncio.ensure_future(_stream_track(track))) for participant in self._pinch_room.remote_participants.values(): for pub in participant.track_publications.values(): if pub.track is not None: start_streaming(pub.track) if not any( isinstance(pub.track, rtc.RemoteAudioTrack) for p in self._pinch_room.remote_participants.values() for pub in p.track_publications.values() ): logger.info( "Waiting for Pinch translation agent to publish audio (timeout=%.0fs)…", _AGENT_TRACK_TIMEOUT, ) @self._pinch_room.on("track_subscribed") def _on_pinch_track_subscribed( track: rtc.Track, publication: rtc.RemoteTrackPublication, participant: rtc.RemoteParticipant, ) -> None: start_streaming(track) try: while not self._stopped: await asyncio.sleep(0.5) except asyncio.CancelledError: pass finally: self._pinch_room.off("track_subscribed", _on_pinch_track_subscribed) for t in streaming_tasks: t.cancel() if streaming_tasks: await asyncio.gather(*streaming_tasks, return_exceptions=True) def _on_pinch_data_received(self, data_packet: rtc.DataPacket) -> None: try: raw = data_packet.data if isinstance(raw, (bytes, bytearray, memoryview)): raw = bytes(raw).decode("utf-8") msg: dict = json.loads(raw) except Exception as exc: logger.warning("Failed to decode Pinch data message: %s", exc) return msg_type = msg.get("type", "") if msg_type not in ("original_transcript", "translated_transcript"): return event = TranscriptEvent( type=msg_type, text=msg.get("text", ""), is_final=bool(msg.get("is_final", False)), language_detected=msg.get("language_detected", ""), timestamp=float(msg.get("timestamp", time.time())), confidence=float(msg.get("confidence", 0.0)), ) logger.debug("Transcript [%s] final=%s %r", event.type, event.is_final, event.text[:80]) self._emit_transcript(event) def _emit_transcript(self, event: TranscriptEvent) -> None: for cb in self._transcript_callbacks: try: cb(event) except Exception as exc: logger.exception("Unhandled exception in transcript callback %r: %s", cb, exc) def __repr__(self) -> str: return ( f"<Translator src={self._options.source_language!r} " f"tgt={self._options.target_language!r} " f"voice={self._options.voice_type!r} " f"started={self._started} stopped={self._stopped}>" )
That's the full code for translator.py
t
Thanks — this is great; I can now do a full review of your README, packaging, and translator implementation. I’ll keep this concise and action-focused so you can prepare a PR that lands quickly. Summary of what I checked • pyproject.toml: package metadata and hatch config present; namespace packaging into the livekit/ tree. • README.md: clear, good examples; missing a few details that reviewers expect. • livekit/plugins/pinch package: init.py, models, log, and the complete translator.py implementation. High-priority fixes and clarifications to address before opening the PR 1. pyproject.toml: fix the HTML-escaped Python requirement. Replace
>=3.10
with
>=3.10
. 2. Ensure the wheel/sdist includes the livekit/plugins/pinch files so importable as
from livekit.plugins.pinch import Translator
. Verify locally with: ◦ python -m build (or hatch build) and pip install the produced wheel in a clean venv, then import and run a quick smoke test. 1. In README and docs, state which LiveKit Agents version you tested against (e.g.,
livekit-agents >=0.8.0
) and Python versions you validated. Code review — translator.py (issues and recommendations) • API/key handling ◦ Good: accepts api_key param, falls back to env var and raises a clear error if missing. ◦ Suggestion: document how to pass a test key vs production key and include a short note about rotating keys or using CI secrets. • Session creation ◦ You correctly translate SDK exceptions to your own exception classes, which is good. ◦ Minor: the call to client.create_session is wrapped in asyncio.to_thread; keep as-is but add a short timeout guard or document expected latency so callers know session creation may block briefly. • Connection retry ◦ Exponential backoff is implemented. Consider logging the exception type in the final error message for easier debugging. ◦ You raise PinchSessionError when all retries fail — good. • Audio bridging (source → Pinch and Pinch → source) ◦ The AudioStream consumption loops are correct and use capture_frame to feed into audio sources. ◦ Potential improvement: handle backpressure explicitly when capture_frame might block (e.g., add small timeout or queue with maxsize to avoid unbounded memory when sink is slow). ◦ You register for existing participant tracks and on "track_subscribed" — good pattern. Make sure unsubscribing is robust in the presence of rapid connect/disconnect (you remove listeners in finally blocks, which is good). • Data channel transcript handling ◦ You decode the data packet safely and guard against malformed JSON, then emit TranscriptEvent objects. Good. ◦ Suggestion: Validate the message fields more strictly (e.g., ensure
text
is a string) or coerce safely to avoid crashes in downstream callbacks. • Callbacks ◦ You catch and log exceptions per callback — good. Consider documenting that callbacks run synchronously on the data thread and that users should avoid long-running sync work (or run their own tasks) to avoid blocking other callbacks. • Lifecycle ◦ start/stop semantics are clear. There is a minor race: if start triggers tasks and stop is called quickly, tasks are cancelled and awaited — works, but add a short doc note about expected time to stop and best practices for cleanup. • Logging ◦ You use a package-scoped logger; good. Consider adding a couple more debug logs for session creation payload and when tracks are published/unpublished (avoid logging secrets). Packaging & tests • Provide at least one unit test that: ◦ Mocks PinchClient.create_session to return a stubbed session (url/token/room_name). ◦ Mocks rtc.Room.connect and remote participant track publications so you can exercise start() and the streaming loops up to the point where you emit a TranscriptEvent via _on_pinch_data_received. • Add a lightweight example script under examples/ that demonstrates: ◦ Creating Translator, registering a transcript callback, starting it with a mocked JobContext or a simple rtc.Room stub, then calling stop(). • Add GitHub Actions workflow to run tests (python-version: 3.10/3.11/3.12) and lint (black/ruff). README: recommended edits (short, actionable) • Add Prerequisites: tested LiveKit Agents version and Python versions. • Installation note: if not yet on PyPI, include pip install from GitHub: ◦ pip install git+https://github.com/pinch-eng/livekit-plugins-pinch.git • Usage: add a short example showing cleanup (await translator.stop()) and an example showing integration with AgentSession/JobContext lifecycle (how you obtain the
room
passed to start()). • Testing: show how to run tests locally and mention that tests mock external network calls. • Compatibility & security note: state that audio and transcripts are sent to the Pinch service and callers should treat them accordingly. PR checklist (do these before opening PR) • [ ] Fix
requires-python
to
>=3.10
. • [ ] Build and test the wheel locally; confirm import path
livekit.plugins.pinch
. • [ ] Add at least one unit test and an examples/demo script. • [ ] Add CI workflow that runs tests and linting. • [ ] Expand README with Prereqs, install-from-Git instructions (if not on PyPI), testing instructions, and lifecycle/cleanup note. • [ ] Ensure translator.py docstrings mention which livekit-agents features are used and any expectations about runtime behavior (timeouts, stopping). PR description template (ready to paste) Title: Add Pinch real-time speech-to-speech translation plugin (livekit/plugins/pinch) Description: This PR adds a Pinch plugin that provides real-time speech-to-speech translation for LiveKit Agents. Features: • Creates a Pinch translation session and connects to the Pinch room. • Bridges audio from source room to Pinch and publishes translated audio back to source room. • Emits transcript events for original and translated text via a simple callback interface. • Converts Pinch SDK errors to plugin-specific exceptions (PinchAuthError, PinchRateLimitError, PinchSessionError). Files of interest: • livekit/plugins/pinch/translator.py — core implementation • livekit/plugins/pinch/models.py — TranslatorOptions, TranscriptEvent • examples/ — demo usage • tests/ — unit tests that mock the Pinch SDK and rtc interactions • pyproject.toml, README.md How to test: 1. Install locally: ◦ pip install -e . 1. Run tests: ◦ pytest 1. Run the example demo: ◦ python examples/demo.py ◦ (Set PINCH_API_KEY to exercise live calls; tests use mocks) Notes for reviewers: • The package uses implicit namespace packaging to install under the
livekit
namespace. • Tests mock external calls to Pinch so maintainers can run CI without secrets. • If maintainers prefer a different packaging strategy (separate top-level package), I can update the packaging. Two quick, targeted questions so I can finish a final pass and craft the exact repo diffs you should open in livekit/agents 1. Do you want this listed as an optional dependency inside the livekit/agents repo (so consumers can install it as an extras group), or do you prefer to keep it published and referenced externally as a separate package? (Both are acceptable; I can prepare the exact dependency lines for either option.) 2. Do you already have tests or an examples/demo script you want me to review? If so paste them (or tell me where they are) and I’ll give a final pre-PR checklist and the exact file diffs to add in livekit/agents (where to update dependency lists or the plugin index). If you want, I can now: • Draft the exact patch/diff you should apply to livekit/agents to list this plugin as an optional dependency and where to add usage notes, or • Review any tests/examples you paste and give final edits. Which would you like next?
n
Yeah, I checked everything. I raised the review like I raised a PR, and three skipped six test cases passed, and it says that the review is required. How long will it take for my PR to get approved?
t
Thanks — and congrats on getting tests passing and opening the PR. I know waiting for review can be stressful; I’ll be direct about what I can and can’t confirm, then give practical next steps you can take to help move it along. Short answer • I cannot confirm an exact review/approval timeframe for LiveKit PRs — there’s no published SLA or timeline in the project sources. • LiveKit is an active open-source project and maintainers do review community PRs, but review times vary. What affects review time (so you know what to expect) • Maintainer availability and current workload. • How complete the PR is (docs, tests, CI green, clear description). • Whether the change affects core behavior, APIs, or packaging (bigger changes take longer). • CI results and skipped tests: maintainers may ask for skipped tests to be fixed or justified. • Whether reviewers need to run integration checks that require credentials or more setup. Practical actions you can take to nudge the review forward • Add a clear PR description and checklist (if you haven’t already). Include: ◦ What the plugin does, how to test it, how to reproduce the skipped tests, and any required credentials (or how to run with mocks). ◦ A short “How to test locally” section with commands you ran (build, pytest, example). • Mark the PR with the appropriate labels (if the repo uses them) and request reviewers explicitly in the GitHub UI — name maintainers or team members if you know them. • Reply to the PR thread with: ◦ A short summary of the current status (e.g., “3 skipped, 6 passed” and why the skipped tests exist). ◦ Any follow-up actions you’re willing to take if maintainers request changes. • Provide deterministic demos/tests: ◦ Add a small test or example that runs without real Pinch credentials (mocking). ◦ If a skipped test requires secrets, explain why it’s skipped and how a reviewer can run it. • Share compatibility notes and CI links in the PR so reviewers don’t have to guess (Python versions tested, livekit-agents version, packaging note about namespace). • If the repo has a community channel (Slack or similar), post a polite note linking the PR and asking for review — maintainers sometimes respond faster there. If you want, I can: • Draft a short, copy-paste PR comment that explains the skipped tests and how to run them locally. • Prepare a follow-up message to post in the LiveKit community (Slack) that summarizes the PR and calls out reviewers politely. • Create a minimal CI/test guidance snippet to add to the PR so reviewers can run the same steps locally. Which of the above would be most helpful right now — a PR comment draft, a Slack/community message, or the short checklist to add to your PR description? Sources: Yeah, but can it run DOOM? | And…we’re Live(Kit)! | LiveKit Community Day