From dc177af3ff9729fb19ea15217d3040ca6d4ca3f6 Mon Sep 17 00:00:00 2001 From: Mathieu Virbel Date: Mon, 4 Aug 2025 12:07:30 -0600 Subject: [PATCH] feat: implement service-specific Modal API keys with auto processor pattern (#528) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: refactor modal API key configuration for better separation of concerns - Split generic MODAL_API_KEY into service-specific keys: - TRANSCRIPT_API_KEY for transcription service - DIARIZATION_API_KEY for diarization service - TRANSLATE_API_KEY for translation service - Remove deprecated *_MODAL_API_KEY settings - Add proper validation to ensure URLs are set when using modal processors - Update README with new configuration format BREAKING CHANGE: Configuration keys have changed. Update your .env file: - TRANSCRIPT_MODAL_API_KEY → TRANSCRIPT_API_KEY - LLM_MODAL_API_KEY → (removed, use TRANSCRIPT_API_KEY) - Add DIARIZATION_API_KEY and TRANSLATE_API_KEY if using those services * fix: update Modal backend configuration to use service-specific API keys - Changed from generic MODAL_API_KEY to service-specific keys: - TRANSCRIPT_MODAL_API_KEY for transcription - DIARIZATION_MODAL_API_KEY for diarization - TRANSLATION_MODAL_API_KEY for translation - Updated audio_transcript_modal.py and audio_diarization_modal.py to use modal_api_key parameter - Updated documentation in README.md, CLAUDE.md, and env.example * feat: implement auto/modal pattern for translation processor - Created TranscriptTranslatorAutoProcessor following the same pattern as transcript/diarization - Created TranscriptTranslatorModalProcessor with TRANSLATION_MODAL_API_KEY support - Added TRANSLATION_BACKEND setting (defaults to "modal") - Updated all imports to use TranscriptTranslatorAutoProcessor instead of TranscriptTranslatorProcessor - Updated env.example with TRANSLATION_BACKEND and TRANSLATION_MODAL_API_KEY - Updated test to expect TranscriptTranslatorModalProcessor name - All tests passing * refactor: simplify transcript_translator base class to match other processors - Moved all implementation from base class to modal processor - Base class now only defines abstract _translate method - Follows the same minimal pattern as audio_diarization and audio_transcript base classes - Updated test mock to use _translate instead of get_translation - All tests passing * chore: clean up settings and improve type annotations - Remove deprecated generic API key variables from settings - Add comments to group Modal-specific settings - Improve type annotations for modal_api_key parameters * fix: typing * fix: passing key to openai * test: fix rtc test failing due to change on transcript It also correctly setup database from sqlite, in case our configuration is setup to postgres. * ci: deactivate translation backend by default * test: fix modal->mock * refactor: implementing igor review, mock to passthrough --- .gitignore | 1 + CLAUDE.md | 4 +- server/env.example | 7 +- server/gpu/modal_deployments/README.md | 12 ++-- server/pyproject.toml | 5 ++ .../reflector/pipelines/main_live_pipeline.py | 4 +- server/reflector/processors/__init__.py | 1 + .../processors/audio_diarization_modal.py | 13 ++-- .../processors/audio_transcript_modal.py | 10 ++- .../processors/transcript_translator.py | 59 ++++------------- .../processors/transcript_translator_auto.py | 32 +++++++++ .../processors/transcript_translator_modal.py | 66 +++++++++++++++++++ .../transcript_translator_passthrough.py | 14 ++++ server/reflector/settings.py | 9 ++- server/reflector/tools/process.py | 4 +- .../tools/process_with_diarization.py | 4 +- server/tests/conftest.py | 38 +++++++---- server/tests/test_processors_pipeline.py | 2 +- server/tests/test_transcripts_rtc_ws.py | 6 +- server/uv.lock | 14 ++++ 20 files changed, 220 insertions(+), 85 deletions(-) create mode 100644 server/reflector/processors/transcript_translator_auto.py create mode 100644 server/reflector/processors/transcript_translator_modal.py create mode 100644 server/reflector/processors/transcript_translator_passthrough.py diff --git a/.gitignore b/.gitignore index 65b990b4..ffff5dfa 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,4 @@ restart-dev.sh data/ www/REFACTOR.md www/reload-frontend +server/test.sqlite diff --git a/CLAUDE.md b/CLAUDE.md index 3ffee5b5..7c71fbf3 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -144,7 +144,9 @@ All endpoints prefixed `/v1/`: **Backend** (`server/.env`): - `DATABASE_URL` - Database connection string - `REDIS_URL` - Redis broker for Celery -- `MODAL_TOKEN_ID`, `MODAL_TOKEN_SECRET` - Modal.com GPU processing +- `TRANSCRIPT_BACKEND=modal` + `TRANSCRIPT_MODAL_API_KEY` - Modal.com transcription +- `DIARIZATION_BACKEND=modal` + `DIARIZATION_MODAL_API_KEY` - Modal.com diarization +- `TRANSLATION_BACKEND=modal` + `TRANSLATION_MODAL_API_KEY` - Modal.com translation - `WHEREBY_API_KEY` - Video platform integration - `REFLECTOR_AUTH_BACKEND` - Authentication method (none, jwt) diff --git a/server/env.example b/server/env.example index 4952a937..70b4b229 100644 --- a/server/env.example +++ b/server/env.example @@ -24,7 +24,6 @@ AUTH_JWT_AUDIENCE= ## Using serverless modal.com (require reflector-gpu-modal deployed) #TRANSCRIPT_BACKEND=modal #TRANSCRIPT_URL=https://xxxxx--reflector-transcriber-web.modal.run -#TRANSLATE_URL=https://xxxxx--reflector-translator-web.modal.run #TRANSCRIPT_MODAL_API_KEY=xxxxx TRANSCRIPT_BACKEND=modal @@ -32,11 +31,13 @@ TRANSCRIPT_URL=https://monadical-sas--reflector-transcriber-web.modal.run TRANSCRIPT_MODAL_API_KEY= ## ======================================================= -## Transcription backend +## Translation backend ## ## Only available in modal atm ## ======================================================= +TRANSLATION_BACKEND=modal TRANSLATE_URL=https://monadical-sas--reflector-translator-web.modal.run +#TRANSLATION_MODAL_API_KEY=xxxxx ## ======================================================= ## LLM backend @@ -59,7 +60,9 @@ LLM_API_KEY=sk- ## To allow diarization, you need to expose expose the files to be dowloded by the pipeline ## ======================================================= DIARIZATION_ENABLED=false +DIARIZATION_BACKEND=modal DIARIZATION_URL=https://monadical-sas--reflector-diarizer-web.modal.run +#DIARIZATION_MODAL_API_KEY=xxxxx ## ======================================================= diff --git a/server/gpu/modal_deployments/README.md b/server/gpu/modal_deployments/README.md index f31810e1..83309f49 100644 --- a/server/gpu/modal_deployments/README.md +++ b/server/gpu/modal_deployments/README.md @@ -24,16 +24,20 @@ $ modal deploy reflector_llm.py └── 🔨 Created web => https://xxxx--reflector-llm-web.modal.run ``` -Then in your reflector api configuration `.env`, you can set theses keys: +Then in your reflector api configuration `.env`, you can set these keys: ``` TRANSCRIPT_BACKEND=modal TRANSCRIPT_URL=https://xxxx--reflector-transcriber-web.modal.run TRANSCRIPT_MODAL_API_KEY=REFLECTOR_APIKEY -LLM_BACKEND=modal -LLM_URL=https://xxxx--reflector-llm-web.modal.run -LLM_MODAL_API_KEY=REFLECTOR_APIKEY +DIARIZATION_BACKEND=modal +DIARIZATION_URL=https://xxxx--reflector-diarizer-web.modal.run +DIARIZATION_MODAL_API_KEY=REFLECTOR_APIKEY + +TRANSLATION_BACKEND=modal +TRANSLATION_URL=https://xxxx--reflector-translator-web.modal.run +TRANSLATION_MODAL_API_KEY=REFLECTOR_APIKEY ``` ## API diff --git a/server/pyproject.toml b/server/pyproject.toml index 4ea9336d..f76f38ac 100644 --- a/server/pyproject.toml +++ b/server/pyproject.toml @@ -40,6 +40,7 @@ dependencies = [ "psycopg2-binary>=2.9.10", "llama-index>=0.12.52", "llama-index-llms-openai-like>=0.4.0", + "pytest-env>=1.1.5", ] [dependency-groups] @@ -83,6 +84,10 @@ packages = ["reflector"] [tool.coverage.run] source = ["reflector"] +[tool.pytest_env] +ENVIRONMENT = "pytest" +DATABASE_URL = "sqlite:///test.sqlite" + [tool.pytest.ini_options] addopts = "-ra -q --disable-pytest-warnings --cov --cov-report html -v" testpaths = ["tests"] diff --git a/server/reflector/pipelines/main_live_pipeline.py b/server/reflector/pipelines/main_live_pipeline.py index 19ff5c4d..0d3af426 100644 --- a/server/reflector/pipelines/main_live_pipeline.py +++ b/server/reflector/pipelines/main_live_pipeline.py @@ -47,7 +47,7 @@ from reflector.processors import ( TranscriptFinalTitleProcessor, TranscriptLinerProcessor, TranscriptTopicDetectorProcessor, - TranscriptTranslatorProcessor, + TranscriptTranslatorAutoProcessor, ) from reflector.processors.audio_waveform_processor import AudioWaveformProcessor from reflector.processors.types import AudioDiarizationInput @@ -361,7 +361,7 @@ class PipelineMainLive(PipelineMainBase): AudioMergeProcessor(), AudioTranscriptAutoProcessor.as_threaded(), TranscriptLinerProcessor(), - TranscriptTranslatorProcessor.as_threaded(callback=self.on_transcript), + TranscriptTranslatorAutoProcessor.as_threaded(callback=self.on_transcript), TranscriptTopicDetectorProcessor.as_threaded(callback=self.on_topic), ] pipeline = Pipeline(*processors) diff --git a/server/reflector/processors/__init__.py b/server/reflector/processors/__init__.py index 0de73350..0aa89f87 100644 --- a/server/reflector/processors/__init__.py +++ b/server/reflector/processors/__init__.py @@ -16,6 +16,7 @@ from .transcript_final_title import TranscriptFinalTitleProcessor # noqa: F401 from .transcript_liner import TranscriptLinerProcessor # noqa: F401 from .transcript_topic_detector import TranscriptTopicDetectorProcessor # noqa: F401 from .transcript_translator import TranscriptTranslatorProcessor # noqa: F401 +from .transcript_translator_auto import TranscriptTranslatorAutoProcessor # noqa: F401 from .types import ( # noqa: F401 AudioFile, FinalLongSummary, diff --git a/server/reflector/processors/audio_diarization_modal.py b/server/reflector/processors/audio_diarization_modal.py index 88c32272..62920239 100644 --- a/server/reflector/processors/audio_diarization_modal.py +++ b/server/reflector/processors/audio_diarization_modal.py @@ -10,12 +10,17 @@ class AudioDiarizationModalProcessor(AudioDiarizationProcessor): INPUT_TYPE = AudioDiarizationInput OUTPUT_TYPE = TitleSummary - def __init__(self, **kwargs): + def __init__(self, modal_api_key: str | None = None, **kwargs): super().__init__(**kwargs) + if not settings.DIARIZATION_URL: + raise Exception( + "DIARIZATION_URL required to use AudioDiarizationModalProcessor" + ) self.diarization_url = settings.DIARIZATION_URL + "/diarize" - self.headers = { - "Authorization": f"Bearer {settings.LLM_MODAL_API_KEY}", - } + self.modal_api_key = modal_api_key + self.headers = {} + if self.modal_api_key: + self.headers["Authorization"] = f"Bearer {self.modal_api_key}" async def _diarize(self, data: AudioDiarizationInput): # Gather diarization data diff --git a/server/reflector/processors/audio_transcript_modal.py b/server/reflector/processors/audio_transcript_modal.py index ebd99a6a..3e53261c 100644 --- a/server/reflector/processors/audio_transcript_modal.py +++ b/server/reflector/processors/audio_transcript_modal.py @@ -21,16 +21,20 @@ from reflector.settings import settings class AudioTranscriptModalProcessor(AudioTranscriptProcessor): - def __init__(self, modal_api_key: str): + def __init__(self, modal_api_key: str | None = None, **kwargs): super().__init__() + if not settings.TRANSCRIPT_URL: + raise Exception( + "TRANSCRIPT_URL required to use AudioTranscriptModalProcessor" + ) self.transcript_url = settings.TRANSCRIPT_URL + "/v1" self.timeout = settings.TRANSCRIPT_TIMEOUT - self.api_key = settings.TRANSCRIPT_MODAL_API_KEY + self.modal_api_key = modal_api_key async def _transcript(self, data: AudioFile): async with AsyncOpenAI( base_url=self.transcript_url, - api_key=self.api_key, + api_key=self.modal_api_key, timeout=self.timeout, ) as client: self.logger.debug(f"Try to transcribe audio {data.name}") diff --git a/server/reflector/processors/transcript_translator.py b/server/reflector/processors/transcript_translator.py index 2489e563..93040907 100644 --- a/server/reflector/processors/transcript_translator.py +++ b/server/reflector/processors/transcript_translator.py @@ -1,9 +1,5 @@ -import httpx - from reflector.processors.base import Processor -from reflector.processors.types import Transcript, TranslationLanguages -from reflector.settings import settings -from reflector.utils.retry import retry +from reflector.processors.types import Transcript class TranscriptTranslatorProcessor(Processor): @@ -17,56 +13,23 @@ class TranscriptTranslatorProcessor(Processor): def __init__(self, **kwargs): super().__init__(**kwargs) self.transcript = None - self.translate_url = settings.TRANSLATE_URL - self.timeout = settings.TRANSLATE_TIMEOUT - self.headers = {"Authorization": f"Bearer {settings.TRANSCRIPT_MODAL_API_KEY}"} async def _push(self, data: Transcript): self.transcript = data await self.flush() - async def get_translation(self, text: str) -> str | None: - # FIXME this should be a processor after, as each user may want - # different languages - - source_language = self.get_pref("audio:source_language", "en") - target_language = self.get_pref("audio:target_language", "en") - if source_language == target_language: - return - - languages = TranslationLanguages() - # Only way to set the target should be the UI element like dropdown. - # Hence, this assert should never fail. - assert languages.is_supported(target_language) - self.logger.debug(f"Try to translate {text=}") - json_payload = { - "text": text, - "source_language": source_language, - "target_language": target_language, - } - - async with httpx.AsyncClient() as client: - response = await retry(client.post)( - self.translate_url + "/translate", - headers=self.headers, - params=json_payload, - timeout=self.timeout, - follow_redirects=True, - logger=self.logger, - ) - response.raise_for_status() - result = response.json()["text"] - - # Sanity check for translation status in the result - if target_language in result: - translation = result[target_language] - self.logger.debug(f"Translation response: {text=}, {translation=}") - return translation + async def _translate(self, text: str) -> str | None: + raise NotImplementedError async def _flush(self): if not self.transcript: return - self.transcript.translation = await self.get_translation( - text=self.transcript.text - ) + + source_language = self.get_pref("audio:source_language", "en") + target_language = self.get_pref("audio:target_language", "en") + if source_language == target_language: + self.transcript.translation = None + else: + self.transcript.translation = await self._translate(self.transcript.text) + await self.emit(self.transcript) diff --git a/server/reflector/processors/transcript_translator_auto.py b/server/reflector/processors/transcript_translator_auto.py new file mode 100644 index 00000000..7032cd8b --- /dev/null +++ b/server/reflector/processors/transcript_translator_auto.py @@ -0,0 +1,32 @@ +import importlib + +from reflector.processors.transcript_translator import TranscriptTranslatorProcessor +from reflector.settings import settings + + +class TranscriptTranslatorAutoProcessor(TranscriptTranslatorProcessor): + _registry = {} + + @classmethod + def register(cls, name, kclass): + cls._registry[name] = kclass + + def __new__(cls, name: str | None = None, **kwargs): + if name is None: + name = settings.TRANSLATION_BACKEND + if name not in cls._registry: + module_name = f"reflector.processors.transcript_translator_{name}" + importlib.import_module(module_name) + + # gather specific configuration for the processor + # search `TRANSLATION_BACKEND_XXX_YYY`, push to constructor as `backend_xxx_yyy` + config = {} + name_upper = name.upper() + settings_prefix = "TRANSLATION_" + config_prefix = f"{settings_prefix}{name_upper}_" + for key, value in settings: + if key.startswith(config_prefix): + config_name = key[len(settings_prefix) :].lower() + config[config_name] = value + + return cls._registry[name](**config | kwargs) diff --git a/server/reflector/processors/transcript_translator_modal.py b/server/reflector/processors/transcript_translator_modal.py new file mode 100644 index 00000000..5630a786 --- /dev/null +++ b/server/reflector/processors/transcript_translator_modal.py @@ -0,0 +1,66 @@ +import httpx + +from reflector.processors.transcript_translator import TranscriptTranslatorProcessor +from reflector.processors.transcript_translator_auto import ( + TranscriptTranslatorAutoProcessor, +) +from reflector.processors.types import TranslationLanguages +from reflector.settings import settings +from reflector.utils.retry import retry + + +class TranscriptTranslatorModalProcessor(TranscriptTranslatorProcessor): + """ + Translate the transcript into the target language using Modal.com + """ + + def __init__(self, modal_api_key: str | None = None, **kwargs): + super().__init__(**kwargs) + if not settings.TRANSLATE_URL: + raise Exception( + "TRANSLATE_URL is required for TranscriptTranslatorModalProcessor" + ) + self.translate_url = settings.TRANSLATE_URL + self.timeout = settings.TRANSLATE_TIMEOUT + self.modal_api_key = modal_api_key + self.headers = {} + if self.modal_api_key: + self.headers["Authorization"] = f"Bearer {self.modal_api_key}" + + async def _translate(self, text: str) -> str | None: + source_language = self.get_pref("audio:source_language", "en") + target_language = self.get_pref("audio:target_language", "en") + + languages = TranslationLanguages() + # Only way to set the target should be the UI element like dropdown. + # Hence, this assert should never fail. + assert languages.is_supported(target_language) + self.logger.debug(f"Try to translate {text=}") + json_payload = { + "text": text, + "source_language": source_language, + "target_language": target_language, + } + + async with httpx.AsyncClient() as client: + response = await retry(client.post)( + self.translate_url + "/translate", + headers=self.headers, + params=json_payload, + timeout=self.timeout, + follow_redirects=True, + logger=self.logger, + ) + response.raise_for_status() + result = response.json()["text"] + + # Sanity check for translation status in the result + if target_language in result: + translation = result[target_language] + else: + translation = None + self.logger.debug(f"Translation response: {text=}, {translation=}") + return translation + + +TranscriptTranslatorAutoProcessor.register("modal", TranscriptTranslatorModalProcessor) diff --git a/server/reflector/processors/transcript_translator_passthrough.py b/server/reflector/processors/transcript_translator_passthrough.py new file mode 100644 index 00000000..ef82aa94 --- /dev/null +++ b/server/reflector/processors/transcript_translator_passthrough.py @@ -0,0 +1,14 @@ +from reflector.processors.transcript_translator import TranscriptTranslatorProcessor +from reflector.processors.transcript_translator_auto import ( + TranscriptTranslatorAutoProcessor, +) + + +class TranscriptTranslatorPassthroughProcessor(TranscriptTranslatorProcessor): + async def _translate(self, text: str) -> None: + return None + + +TranscriptTranslatorAutoProcessor.register( + "passthrough", TranscriptTranslatorPassthroughProcessor +) diff --git a/server/reflector/settings.py b/server/reflector/settings.py index 30af270b..e564a319 100644 --- a/server/reflector/settings.py +++ b/server/reflector/settings.py @@ -25,7 +25,7 @@ class Settings(BaseSettings): TRANSCRIPT_URL: str | None = None TRANSCRIPT_TIMEOUT: int = 90 - # Audio transcription modal.com configuration + # Audio Transcription: modal backend TRANSCRIPT_MODAL_API_KEY: str | None = None # Audio transcription storage @@ -38,9 +38,13 @@ class Settings(BaseSettings): TRANSCRIPT_STORAGE_AWS_SECRET_ACCESS_KEY: str | None = None # Translate into the target language + TRANSLATION_BACKEND: str = "passthrough" TRANSLATE_URL: str | None = None TRANSLATE_TIMEOUT: int = 90 + # Translation: modal backend + TRANSLATE_MODAL_API_KEY: str | None = None + # LLM LLM_MODEL: str = "microsoft/phi-4" LLM_URL: str | None = None @@ -52,6 +56,9 @@ class Settings(BaseSettings): DIARIZATION_BACKEND: str = "modal" DIARIZATION_URL: str | None = None + # Diarization: modal backend + DIARIZATION_MODAL_API_KEY: str | None = None + # Sentry SENTRY_DSN: str | None = None diff --git a/server/reflector/tools/process.py b/server/reflector/tools/process.py index 2ff72677..91ac8e7c 100644 --- a/server/reflector/tools/process.py +++ b/server/reflector/tools/process.py @@ -13,7 +13,7 @@ from reflector.processors import ( TranscriptFinalTitleProcessor, TranscriptLinerProcessor, TranscriptTopicDetectorProcessor, - TranscriptTranslatorProcessor, + TranscriptTranslatorAutoProcessor, ) from reflector.processors.base import BroadcastProcessor @@ -31,7 +31,7 @@ async def process_audio_file( AudioMergeProcessor(), AudioTranscriptAutoProcessor.as_threaded(), TranscriptLinerProcessor(), - TranscriptTranslatorProcessor.as_threaded(), + TranscriptTranslatorAutoProcessor.as_threaded(), ] if not only_transcript: processors += [ diff --git a/server/reflector/tools/process_with_diarization.py b/server/reflector/tools/process_with_diarization.py index c7310334..49d5cd83 100644 --- a/server/reflector/tools/process_with_diarization.py +++ b/server/reflector/tools/process_with_diarization.py @@ -27,7 +27,7 @@ from reflector.processors import ( TranscriptFinalTitleProcessor, TranscriptLinerProcessor, TranscriptTopicDetectorProcessor, - TranscriptTranslatorProcessor, + TranscriptTranslatorAutoProcessor, ) from reflector.processors.base import BroadcastProcessor, Processor from reflector.processors.types import ( @@ -103,7 +103,7 @@ async def process_audio_file_with_diarization( processors += [ TranscriptLinerProcessor(), - TranscriptTranslatorProcessor.as_threaded(), + TranscriptTranslatorAutoProcessor.as_threaded(), ] if not only_transcript: diff --git a/server/tests/conftest.py b/server/tests/conftest.py index 434e7dea..42d6d27b 100644 --- a/server/tests/conftest.py +++ b/server/tests/conftest.py @@ -7,15 +7,11 @@ import pytest @pytest.fixture(scope="function", autouse=True) @pytest.mark.asyncio async def setup_database(): - from reflector.settings import settings + from reflector.db import engine, metadata # noqa - with NamedTemporaryFile() as f: - settings.DATABASE_URL = f"sqlite:///{f.name}" - from reflector.db import engine, metadata - - metadata.create_all(bind=engine) - - yield + metadata.drop_all(bind=engine) + metadata.create_all(bind=engine) + yield @pytest.fixture @@ -33,9 +29,6 @@ def dummy_processors(): patch( "reflector.processors.transcript_final_summary.TranscriptFinalSummaryProcessor.get_short_summary" ) as mock_short_summary, - patch( - "reflector.processors.transcript_translator.TranscriptTranslatorProcessor.get_translation" - ) as mock_translate, ): from reflector.processors.transcript_topic_detector import TopicResponse @@ -45,9 +38,7 @@ def dummy_processors(): mock_title.return_value = "LLM Title" mock_long_summary.return_value = "LLM LONG SUMMARY" mock_short_summary.return_value = "LLM SHORT SUMMARY" - mock_translate.return_value = "Bonjour le monde" yield ( - mock_translate, mock_topic, mock_title, mock_long_summary, @@ -105,6 +96,27 @@ async def dummy_diarization(): yield +@pytest.fixture +async def dummy_transcript_translator(): + from reflector.processors.transcript_translator import TranscriptTranslatorProcessor + + class TestTranscriptTranslatorProcessor(TranscriptTranslatorProcessor): + async def _translate(self, text: str) -> str: + source_language = self.get_pref("audio:source_language", "en") + target_language = self.get_pref("audio:target_language", "en") + return f"{source_language}:{target_language}:{text}" + + def mock_new(cls, *args, **kwargs): + return TestTranscriptTranslatorProcessor(*args, **kwargs) + + with patch( + "reflector.processors.transcript_translator_auto" + ".TranscriptTranslatorAutoProcessor.__new__", + mock_new, + ): + yield + + @pytest.fixture async def dummy_llm(): from reflector.llm import LLM diff --git a/server/tests/test_processors_pipeline.py b/server/tests/test_processors_pipeline.py index a1787c49..accd80c6 100644 --- a/server/tests/test_processors_pipeline.py +++ b/server/tests/test_processors_pipeline.py @@ -33,7 +33,7 @@ async def test_basic_process( # validate the events assert marks["TranscriptLinerProcessor"] == 1 - assert marks["TranscriptTranslatorProcessor"] == 1 + assert marks["TranscriptTranslatorPassthroughProcessor"] == 1 assert marks["TranscriptTopicDetectorProcessor"] == 1 assert marks["TranscriptFinalSummaryProcessor"] == 1 assert marks["TranscriptFinalTitleProcessor"] == 1 diff --git a/server/tests/test_transcripts_rtc_ws.py b/server/tests/test_transcripts_rtc_ws.py index a8406337..f9dfe7f9 100644 --- a/server/tests/test_transcripts_rtc_ws.py +++ b/server/tests/test_transcripts_rtc_ws.py @@ -67,6 +67,7 @@ async def test_transcript_rtc_and_websocket( dummy_transcript, dummy_processors, dummy_diarization, + dummy_transcript_translator, dummy_storage, fake_mp3_upload, appserver, @@ -164,7 +165,7 @@ async def test_transcript_rtc_and_websocket( assert "TRANSCRIPT" in eventnames ev = events[eventnames.index("TRANSCRIPT")] assert ev["data"]["text"].startswith("Hello world.") - assert ev["data"]["translation"] == "Bonjour le monde" + assert ev["data"]["translation"] is None assert "TOPIC" in eventnames ev = events[eventnames.index("TOPIC")] @@ -224,6 +225,7 @@ async def test_transcript_rtc_and_websocket_and_fr( dummy_transcript, dummy_processors, dummy_diarization, + dummy_transcript_translator, dummy_storage, fake_mp3_upload, appserver, @@ -330,7 +332,7 @@ async def test_transcript_rtc_and_websocket_and_fr( assert "TRANSCRIPT" in eventnames ev = events[eventnames.index("TRANSCRIPT")] assert ev["data"]["text"].startswith("Hello world.") - assert ev["data"]["translation"] == "Bonjour le monde" + assert ev["data"]["translation"] == "en:fr:Hello world." assert "TOPIC" in eventnames ev = events[eventnames.index("TOPIC")] diff --git a/server/uv.lock b/server/uv.lock index 23725ca8..a1b205b2 100644 --- a/server/uv.lock +++ b/server/uv.lock @@ -2428,6 +2428,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/57/79/9dae84c244dabebca6a952e098d6ac9d13719b701fc5323ba6d00abc675a/pytest_docker_tools-3.1.9-py2.py3-none-any.whl", hash = "sha256:36f8e88d56d84ea177df68a175673681243dd991d2807fbf551d90f60341bfdb", size = 29268, upload-time = "2025-03-16T13:48:22.184Z" }, ] +[[package]] +name = "pytest-env" +version = "1.1.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1f/31/27f28431a16b83cab7a636dce59cf397517807d247caa38ee67d65e71ef8/pytest_env-1.1.5.tar.gz", hash = "sha256:91209840aa0e43385073ac464a554ad2947cc2fd663a9debf88d03b01e0cc1cf", size = 8911, upload-time = "2024-09-17T22:39:18.566Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/b8/87cfb16045c9d4092cfcf526135d73b88101aac83bc1adcf82dfb5fd3833/pytest_env-1.1.5-py3-none-any.whl", hash = "sha256:ce90cf8772878515c24b31cd97c7fa1f4481cd68d588419fd45f10ecaee6bc30", size = 6141, upload-time = "2024-09-17T22:39:16.942Z" }, +] + [[package]] name = "pytest-httpx" version = "0.34.0" @@ -2636,6 +2648,7 @@ dependencies = [ { name = "protobuf" }, { name = "psycopg2-binary" }, { name = "pydantic-settings" }, + { name = "pytest-env" }, { name = "python-jose", extra = ["cryptography"] }, { name = "python-multipart" }, { name = "redis" }, @@ -2699,6 +2712,7 @@ requires-dist = [ { name = "protobuf", specifier = ">=4.24.3" }, { name = "psycopg2-binary", specifier = ">=2.9.10" }, { name = "pydantic-settings", specifier = ">=2.0.2" }, + { name = "pytest-env", specifier = ">=1.1.5" }, { name = "python-jose", extras = ["cryptography"], specifier = ">=3.3.0" }, { name = "python-multipart", specifier = ">=0.0.6" }, { name = "redis", specifier = ">=5.0.1" },