mirror of
https://github.com/Monadical-SAS/reflector.git
synced 2025-12-21 12:49:06 +00:00
Compare commits
4 Commits
mathieu/fe
...
v0.6.0
| Author | SHA1 | Date | |
|---|---|---|---|
| 267b7401ea | |||
| aea9de393c | |||
| dc177af3ff | |||
| 5bd8233657 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -13,3 +13,4 @@ restart-dev.sh
|
|||||||
data/
|
data/
|
||||||
www/REFACTOR.md
|
www/REFACTOR.md
|
||||||
www/reload-frontend
|
www/reload-frontend
|
||||||
|
server/test.sqlite
|
||||||
|
|||||||
20
CHANGELOG.md
20
CHANGELOG.md
@@ -1,5 +1,25 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## [0.6.0](https://github.com/Monadical-SAS/reflector/compare/v0.5.0...v0.6.0) (2025-08-05)
|
||||||
|
|
||||||
|
|
||||||
|
### ⚠ BREAKING CHANGES
|
||||||
|
|
||||||
|
* 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
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* implement service-specific Modal API keys with auto processor pattern ([#528](https://github.com/Monadical-SAS/reflector/issues/528)) ([650befb](https://github.com/Monadical-SAS/reflector/commit/650befb291c47a1f49e94a01ab37d8fdfcd2b65d))
|
||||||
|
* use llamaindex everywhere ([#525](https://github.com/Monadical-SAS/reflector/issues/525)) ([3141d17](https://github.com/Monadical-SAS/reflector/commit/3141d172bc4d3b3d533370c8e6e351ea762169bf))
|
||||||
|
|
||||||
|
|
||||||
|
### Miscellaneous Chores
|
||||||
|
|
||||||
|
* **main:** release 0.6.0 ([ecdbf00](https://github.com/Monadical-SAS/reflector/commit/ecdbf003ea2476c3e95fd231adaeb852f2943df0))
|
||||||
|
|
||||||
## [0.5.0](https://github.com/Monadical-SAS/reflector/compare/v0.4.0...v0.5.0) (2025-07-31)
|
## [0.5.0](https://github.com/Monadical-SAS/reflector/compare/v0.4.0...v0.5.0) (2025-07-31)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -144,7 +144,9 @@ All endpoints prefixed `/v1/`:
|
|||||||
**Backend** (`server/.env`):
|
**Backend** (`server/.env`):
|
||||||
- `DATABASE_URL` - Database connection string
|
- `DATABASE_URL` - Database connection string
|
||||||
- `REDIS_URL` - Redis broker for Celery
|
- `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
|
- `WHEREBY_API_KEY` - Video platform integration
|
||||||
- `REFLECTOR_AUTH_BACKEND` - Authentication method (none, jwt)
|
- `REFLECTOR_AUTH_BACKEND` - Authentication method (none, jwt)
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ AUTH_JWT_AUDIENCE=
|
|||||||
## Using serverless modal.com (require reflector-gpu-modal deployed)
|
## Using serverless modal.com (require reflector-gpu-modal deployed)
|
||||||
#TRANSCRIPT_BACKEND=modal
|
#TRANSCRIPT_BACKEND=modal
|
||||||
#TRANSCRIPT_URL=https://xxxxx--reflector-transcriber-web.modal.run
|
#TRANSCRIPT_URL=https://xxxxx--reflector-transcriber-web.modal.run
|
||||||
#TRANSLATE_URL=https://xxxxx--reflector-translator-web.modal.run
|
|
||||||
#TRANSCRIPT_MODAL_API_KEY=xxxxx
|
#TRANSCRIPT_MODAL_API_KEY=xxxxx
|
||||||
|
|
||||||
TRANSCRIPT_BACKEND=modal
|
TRANSCRIPT_BACKEND=modal
|
||||||
@@ -32,11 +31,13 @@ TRANSCRIPT_URL=https://monadical-sas--reflector-transcriber-web.modal.run
|
|||||||
TRANSCRIPT_MODAL_API_KEY=
|
TRANSCRIPT_MODAL_API_KEY=
|
||||||
|
|
||||||
## =======================================================
|
## =======================================================
|
||||||
## Transcription backend
|
## Translation backend
|
||||||
##
|
##
|
||||||
## Only available in modal atm
|
## Only available in modal atm
|
||||||
## =======================================================
|
## =======================================================
|
||||||
|
TRANSLATION_BACKEND=modal
|
||||||
TRANSLATE_URL=https://monadical-sas--reflector-translator-web.modal.run
|
TRANSLATE_URL=https://monadical-sas--reflector-translator-web.modal.run
|
||||||
|
#TRANSLATION_MODAL_API_KEY=xxxxx
|
||||||
|
|
||||||
## =======================================================
|
## =======================================================
|
||||||
## LLM backend
|
## 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
|
## To allow diarization, you need to expose expose the files to be dowloded by the pipeline
|
||||||
## =======================================================
|
## =======================================================
|
||||||
DIARIZATION_ENABLED=false
|
DIARIZATION_ENABLED=false
|
||||||
|
DIARIZATION_BACKEND=modal
|
||||||
DIARIZATION_URL=https://monadical-sas--reflector-diarizer-web.modal.run
|
DIARIZATION_URL=https://monadical-sas--reflector-diarizer-web.modal.run
|
||||||
|
#DIARIZATION_MODAL_API_KEY=xxxxx
|
||||||
|
|
||||||
|
|
||||||
## =======================================================
|
## =======================================================
|
||||||
|
|||||||
@@ -24,16 +24,20 @@ $ modal deploy reflector_llm.py
|
|||||||
└── 🔨 Created web => https://xxxx--reflector-llm-web.modal.run
|
└── 🔨 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_BACKEND=modal
|
||||||
TRANSCRIPT_URL=https://xxxx--reflector-transcriber-web.modal.run
|
TRANSCRIPT_URL=https://xxxx--reflector-transcriber-web.modal.run
|
||||||
TRANSCRIPT_MODAL_API_KEY=REFLECTOR_APIKEY
|
TRANSCRIPT_MODAL_API_KEY=REFLECTOR_APIKEY
|
||||||
|
|
||||||
LLM_BACKEND=modal
|
DIARIZATION_BACKEND=modal
|
||||||
LLM_URL=https://xxxx--reflector-llm-web.modal.run
|
DIARIZATION_URL=https://xxxx--reflector-diarizer-web.modal.run
|
||||||
LLM_MODAL_API_KEY=REFLECTOR_APIKEY
|
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
|
## API
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ dependencies = [
|
|||||||
"psycopg2-binary>=2.9.10",
|
"psycopg2-binary>=2.9.10",
|
||||||
"llama-index>=0.12.52",
|
"llama-index>=0.12.52",
|
||||||
"llama-index-llms-openai-like>=0.4.0",
|
"llama-index-llms-openai-like>=0.4.0",
|
||||||
|
"pytest-env>=1.1.5",
|
||||||
]
|
]
|
||||||
|
|
||||||
[dependency-groups]
|
[dependency-groups]
|
||||||
@@ -83,6 +84,10 @@ packages = ["reflector"]
|
|||||||
[tool.coverage.run]
|
[tool.coverage.run]
|
||||||
source = ["reflector"]
|
source = ["reflector"]
|
||||||
|
|
||||||
|
[tool.pytest_env]
|
||||||
|
ENVIRONMENT = "pytest"
|
||||||
|
DATABASE_URL = "sqlite:///test.sqlite"
|
||||||
|
|
||||||
[tool.pytest.ini_options]
|
[tool.pytest.ini_options]
|
||||||
addopts = "-ra -q --disable-pytest-warnings --cov --cov-report html -v"
|
addopts = "-ra -q --disable-pytest-warnings --cov --cov-report html -v"
|
||||||
testpaths = ["tests"]
|
testpaths = ["tests"]
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ from reflector.processors import (
|
|||||||
TranscriptFinalTitleProcessor,
|
TranscriptFinalTitleProcessor,
|
||||||
TranscriptLinerProcessor,
|
TranscriptLinerProcessor,
|
||||||
TranscriptTopicDetectorProcessor,
|
TranscriptTopicDetectorProcessor,
|
||||||
TranscriptTranslatorProcessor,
|
TranscriptTranslatorAutoProcessor,
|
||||||
)
|
)
|
||||||
from reflector.processors.audio_waveform_processor import AudioWaveformProcessor
|
from reflector.processors.audio_waveform_processor import AudioWaveformProcessor
|
||||||
from reflector.processors.types import AudioDiarizationInput
|
from reflector.processors.types import AudioDiarizationInput
|
||||||
@@ -361,7 +361,7 @@ class PipelineMainLive(PipelineMainBase):
|
|||||||
AudioMergeProcessor(),
|
AudioMergeProcessor(),
|
||||||
AudioTranscriptAutoProcessor.as_threaded(),
|
AudioTranscriptAutoProcessor.as_threaded(),
|
||||||
TranscriptLinerProcessor(),
|
TranscriptLinerProcessor(),
|
||||||
TranscriptTranslatorProcessor.as_threaded(callback=self.on_transcript),
|
TranscriptTranslatorAutoProcessor.as_threaded(callback=self.on_transcript),
|
||||||
TranscriptTopicDetectorProcessor.as_threaded(callback=self.on_topic),
|
TranscriptTopicDetectorProcessor.as_threaded(callback=self.on_topic),
|
||||||
]
|
]
|
||||||
pipeline = Pipeline(*processors)
|
pipeline = Pipeline(*processors)
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ from .transcript_final_title import TranscriptFinalTitleProcessor # noqa: F401
|
|||||||
from .transcript_liner import TranscriptLinerProcessor # noqa: F401
|
from .transcript_liner import TranscriptLinerProcessor # noqa: F401
|
||||||
from .transcript_topic_detector import TranscriptTopicDetectorProcessor # noqa: F401
|
from .transcript_topic_detector import TranscriptTopicDetectorProcessor # noqa: F401
|
||||||
from .transcript_translator import TranscriptTranslatorProcessor # noqa: F401
|
from .transcript_translator import TranscriptTranslatorProcessor # noqa: F401
|
||||||
|
from .transcript_translator_auto import TranscriptTranslatorAutoProcessor # noqa: F401
|
||||||
from .types import ( # noqa: F401
|
from .types import ( # noqa: F401
|
||||||
AudioFile,
|
AudioFile,
|
||||||
FinalLongSummary,
|
FinalLongSummary,
|
||||||
|
|||||||
@@ -10,12 +10,17 @@ class AudioDiarizationModalProcessor(AudioDiarizationProcessor):
|
|||||||
INPUT_TYPE = AudioDiarizationInput
|
INPUT_TYPE = AudioDiarizationInput
|
||||||
OUTPUT_TYPE = TitleSummary
|
OUTPUT_TYPE = TitleSummary
|
||||||
|
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, modal_api_key: str | None = None, **kwargs):
|
||||||
super().__init__(**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.diarization_url = settings.DIARIZATION_URL + "/diarize"
|
||||||
self.headers = {
|
self.modal_api_key = modal_api_key
|
||||||
"Authorization": f"Bearer {settings.LLM_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):
|
async def _diarize(self, data: AudioDiarizationInput):
|
||||||
# Gather diarization data
|
# Gather diarization data
|
||||||
|
|||||||
@@ -21,16 +21,20 @@ from reflector.settings import settings
|
|||||||
|
|
||||||
|
|
||||||
class AudioTranscriptModalProcessor(AudioTranscriptProcessor):
|
class AudioTranscriptModalProcessor(AudioTranscriptProcessor):
|
||||||
def __init__(self, modal_api_key: str):
|
def __init__(self, modal_api_key: str | None = None, **kwargs):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
if not settings.TRANSCRIPT_URL:
|
||||||
|
raise Exception(
|
||||||
|
"TRANSCRIPT_URL required to use AudioTranscriptModalProcessor"
|
||||||
|
)
|
||||||
self.transcript_url = settings.TRANSCRIPT_URL + "/v1"
|
self.transcript_url = settings.TRANSCRIPT_URL + "/v1"
|
||||||
self.timeout = settings.TRANSCRIPT_TIMEOUT
|
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 def _transcript(self, data: AudioFile):
|
||||||
async with AsyncOpenAI(
|
async with AsyncOpenAI(
|
||||||
base_url=self.transcript_url,
|
base_url=self.transcript_url,
|
||||||
api_key=self.api_key,
|
api_key=self.modal_api_key,
|
||||||
timeout=self.timeout,
|
timeout=self.timeout,
|
||||||
) as client:
|
) as client:
|
||||||
self.logger.debug(f"Try to transcribe audio {data.name}")
|
self.logger.debug(f"Try to transcribe audio {data.name}")
|
||||||
|
|||||||
@@ -1,9 +1,5 @@
|
|||||||
import httpx
|
|
||||||
|
|
||||||
from reflector.processors.base import Processor
|
from reflector.processors.base import Processor
|
||||||
from reflector.processors.types import Transcript, TranslationLanguages
|
from reflector.processors.types import Transcript
|
||||||
from reflector.settings import settings
|
|
||||||
from reflector.utils.retry import retry
|
|
||||||
|
|
||||||
|
|
||||||
class TranscriptTranslatorProcessor(Processor):
|
class TranscriptTranslatorProcessor(Processor):
|
||||||
@@ -17,56 +13,23 @@ class TranscriptTranslatorProcessor(Processor):
|
|||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
super().__init__(**kwargs)
|
super().__init__(**kwargs)
|
||||||
self.transcript = None
|
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):
|
async def _push(self, data: Transcript):
|
||||||
self.transcript = data
|
self.transcript = data
|
||||||
await self.flush()
|
await self.flush()
|
||||||
|
|
||||||
async def get_translation(self, text: str) -> str | None:
|
async def _translate(self, text: str) -> str | None:
|
||||||
# FIXME this should be a processor after, as each user may want
|
raise NotImplementedError
|
||||||
# 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 _flush(self):
|
async def _flush(self):
|
||||||
if not self.transcript:
|
if not self.transcript:
|
||||||
return
|
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)
|
await self.emit(self.transcript)
|
||||||
|
|||||||
32
server/reflector/processors/transcript_translator_auto.py
Normal file
32
server/reflector/processors/transcript_translator_auto.py
Normal file
@@ -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)
|
||||||
66
server/reflector/processors/transcript_translator_modal.py
Normal file
66
server/reflector/processors/transcript_translator_modal.py
Normal file
@@ -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)
|
||||||
@@ -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
|
||||||
|
)
|
||||||
@@ -25,7 +25,7 @@ class Settings(BaseSettings):
|
|||||||
TRANSCRIPT_URL: str | None = None
|
TRANSCRIPT_URL: str | None = None
|
||||||
TRANSCRIPT_TIMEOUT: int = 90
|
TRANSCRIPT_TIMEOUT: int = 90
|
||||||
|
|
||||||
# Audio transcription modal.com configuration
|
# Audio Transcription: modal backend
|
||||||
TRANSCRIPT_MODAL_API_KEY: str | None = None
|
TRANSCRIPT_MODAL_API_KEY: str | None = None
|
||||||
|
|
||||||
# Audio transcription storage
|
# Audio transcription storage
|
||||||
@@ -38,9 +38,13 @@ class Settings(BaseSettings):
|
|||||||
TRANSCRIPT_STORAGE_AWS_SECRET_ACCESS_KEY: str | None = None
|
TRANSCRIPT_STORAGE_AWS_SECRET_ACCESS_KEY: str | None = None
|
||||||
|
|
||||||
# Translate into the target language
|
# Translate into the target language
|
||||||
|
TRANSLATION_BACKEND: str = "passthrough"
|
||||||
TRANSLATE_URL: str | None = None
|
TRANSLATE_URL: str | None = None
|
||||||
TRANSLATE_TIMEOUT: int = 90
|
TRANSLATE_TIMEOUT: int = 90
|
||||||
|
|
||||||
|
# Translation: modal backend
|
||||||
|
TRANSLATE_MODAL_API_KEY: str | None = None
|
||||||
|
|
||||||
# LLM
|
# LLM
|
||||||
LLM_MODEL: str = "microsoft/phi-4"
|
LLM_MODEL: str = "microsoft/phi-4"
|
||||||
LLM_URL: str | None = None
|
LLM_URL: str | None = None
|
||||||
@@ -52,6 +56,9 @@ class Settings(BaseSettings):
|
|||||||
DIARIZATION_BACKEND: str = "modal"
|
DIARIZATION_BACKEND: str = "modal"
|
||||||
DIARIZATION_URL: str | None = None
|
DIARIZATION_URL: str | None = None
|
||||||
|
|
||||||
|
# Diarization: modal backend
|
||||||
|
DIARIZATION_MODAL_API_KEY: str | None = None
|
||||||
|
|
||||||
# Sentry
|
# Sentry
|
||||||
SENTRY_DSN: str | None = None
|
SENTRY_DSN: str | None = None
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ from reflector.processors import (
|
|||||||
TranscriptFinalTitleProcessor,
|
TranscriptFinalTitleProcessor,
|
||||||
TranscriptLinerProcessor,
|
TranscriptLinerProcessor,
|
||||||
TranscriptTopicDetectorProcessor,
|
TranscriptTopicDetectorProcessor,
|
||||||
TranscriptTranslatorProcessor,
|
TranscriptTranslatorAutoProcessor,
|
||||||
)
|
)
|
||||||
from reflector.processors.base import BroadcastProcessor
|
from reflector.processors.base import BroadcastProcessor
|
||||||
|
|
||||||
@@ -31,7 +31,7 @@ async def process_audio_file(
|
|||||||
AudioMergeProcessor(),
|
AudioMergeProcessor(),
|
||||||
AudioTranscriptAutoProcessor.as_threaded(),
|
AudioTranscriptAutoProcessor.as_threaded(),
|
||||||
TranscriptLinerProcessor(),
|
TranscriptLinerProcessor(),
|
||||||
TranscriptTranslatorProcessor.as_threaded(),
|
TranscriptTranslatorAutoProcessor.as_threaded(),
|
||||||
]
|
]
|
||||||
if not only_transcript:
|
if not only_transcript:
|
||||||
processors += [
|
processors += [
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ from reflector.processors import (
|
|||||||
TranscriptFinalTitleProcessor,
|
TranscriptFinalTitleProcessor,
|
||||||
TranscriptLinerProcessor,
|
TranscriptLinerProcessor,
|
||||||
TranscriptTopicDetectorProcessor,
|
TranscriptTopicDetectorProcessor,
|
||||||
TranscriptTranslatorProcessor,
|
TranscriptTranslatorAutoProcessor,
|
||||||
)
|
)
|
||||||
from reflector.processors.base import BroadcastProcessor, Processor
|
from reflector.processors.base import BroadcastProcessor, Processor
|
||||||
from reflector.processors.types import (
|
from reflector.processors.types import (
|
||||||
@@ -103,7 +103,7 @@ async def process_audio_file_with_diarization(
|
|||||||
|
|
||||||
processors += [
|
processors += [
|
||||||
TranscriptLinerProcessor(),
|
TranscriptLinerProcessor(),
|
||||||
TranscriptTranslatorProcessor.as_threaded(),
|
TranscriptTranslatorAutoProcessor.as_threaded(),
|
||||||
]
|
]
|
||||||
|
|
||||||
if not only_transcript:
|
if not only_transcript:
|
||||||
|
|||||||
@@ -7,15 +7,11 @@ import pytest
|
|||||||
@pytest.fixture(scope="function", autouse=True)
|
@pytest.fixture(scope="function", autouse=True)
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def setup_database():
|
async def setup_database():
|
||||||
from reflector.settings import settings
|
from reflector.db import engine, metadata # noqa
|
||||||
|
|
||||||
with NamedTemporaryFile() as f:
|
metadata.drop_all(bind=engine)
|
||||||
settings.DATABASE_URL = f"sqlite:///{f.name}"
|
metadata.create_all(bind=engine)
|
||||||
from reflector.db import engine, metadata
|
yield
|
||||||
|
|
||||||
metadata.create_all(bind=engine)
|
|
||||||
|
|
||||||
yield
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
@@ -33,9 +29,6 @@ def dummy_processors():
|
|||||||
patch(
|
patch(
|
||||||
"reflector.processors.transcript_final_summary.TranscriptFinalSummaryProcessor.get_short_summary"
|
"reflector.processors.transcript_final_summary.TranscriptFinalSummaryProcessor.get_short_summary"
|
||||||
) as mock_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
|
from reflector.processors.transcript_topic_detector import TopicResponse
|
||||||
|
|
||||||
@@ -45,9 +38,7 @@ def dummy_processors():
|
|||||||
mock_title.return_value = "LLM Title"
|
mock_title.return_value = "LLM Title"
|
||||||
mock_long_summary.return_value = "LLM LONG SUMMARY"
|
mock_long_summary.return_value = "LLM LONG SUMMARY"
|
||||||
mock_short_summary.return_value = "LLM SHORT SUMMARY"
|
mock_short_summary.return_value = "LLM SHORT SUMMARY"
|
||||||
mock_translate.return_value = "Bonjour le monde"
|
|
||||||
yield (
|
yield (
|
||||||
mock_translate,
|
|
||||||
mock_topic,
|
mock_topic,
|
||||||
mock_title,
|
mock_title,
|
||||||
mock_long_summary,
|
mock_long_summary,
|
||||||
@@ -105,6 +96,27 @@ async def dummy_diarization():
|
|||||||
yield
|
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
|
@pytest.fixture
|
||||||
async def dummy_llm():
|
async def dummy_llm():
|
||||||
from reflector.llm import LLM
|
from reflector.llm import LLM
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ async def test_basic_process(
|
|||||||
|
|
||||||
# validate the events
|
# validate the events
|
||||||
assert marks["TranscriptLinerProcessor"] == 1
|
assert marks["TranscriptLinerProcessor"] == 1
|
||||||
assert marks["TranscriptTranslatorProcessor"] == 1
|
assert marks["TranscriptTranslatorPassthroughProcessor"] == 1
|
||||||
assert marks["TranscriptTopicDetectorProcessor"] == 1
|
assert marks["TranscriptTopicDetectorProcessor"] == 1
|
||||||
assert marks["TranscriptFinalSummaryProcessor"] == 1
|
assert marks["TranscriptFinalSummaryProcessor"] == 1
|
||||||
assert marks["TranscriptFinalTitleProcessor"] == 1
|
assert marks["TranscriptFinalTitleProcessor"] == 1
|
||||||
|
|||||||
@@ -67,6 +67,7 @@ async def test_transcript_rtc_and_websocket(
|
|||||||
dummy_transcript,
|
dummy_transcript,
|
||||||
dummy_processors,
|
dummy_processors,
|
||||||
dummy_diarization,
|
dummy_diarization,
|
||||||
|
dummy_transcript_translator,
|
||||||
dummy_storage,
|
dummy_storage,
|
||||||
fake_mp3_upload,
|
fake_mp3_upload,
|
||||||
appserver,
|
appserver,
|
||||||
@@ -164,7 +165,7 @@ async def test_transcript_rtc_and_websocket(
|
|||||||
assert "TRANSCRIPT" in eventnames
|
assert "TRANSCRIPT" in eventnames
|
||||||
ev = events[eventnames.index("TRANSCRIPT")]
|
ev = events[eventnames.index("TRANSCRIPT")]
|
||||||
assert ev["data"]["text"].startswith("Hello world.")
|
assert ev["data"]["text"].startswith("Hello world.")
|
||||||
assert ev["data"]["translation"] == "Bonjour le monde"
|
assert ev["data"]["translation"] is None
|
||||||
|
|
||||||
assert "TOPIC" in eventnames
|
assert "TOPIC" in eventnames
|
||||||
ev = events[eventnames.index("TOPIC")]
|
ev = events[eventnames.index("TOPIC")]
|
||||||
@@ -224,6 +225,7 @@ async def test_transcript_rtc_and_websocket_and_fr(
|
|||||||
dummy_transcript,
|
dummy_transcript,
|
||||||
dummy_processors,
|
dummy_processors,
|
||||||
dummy_diarization,
|
dummy_diarization,
|
||||||
|
dummy_transcript_translator,
|
||||||
dummy_storage,
|
dummy_storage,
|
||||||
fake_mp3_upload,
|
fake_mp3_upload,
|
||||||
appserver,
|
appserver,
|
||||||
@@ -330,7 +332,7 @@ async def test_transcript_rtc_and_websocket_and_fr(
|
|||||||
assert "TRANSCRIPT" in eventnames
|
assert "TRANSCRIPT" in eventnames
|
||||||
ev = events[eventnames.index("TRANSCRIPT")]
|
ev = events[eventnames.index("TRANSCRIPT")]
|
||||||
assert ev["data"]["text"].startswith("Hello world.")
|
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
|
assert "TOPIC" in eventnames
|
||||||
ev = events[eventnames.index("TOPIC")]
|
ev = events[eventnames.index("TOPIC")]
|
||||||
|
|||||||
14
server/uv.lock
generated
14
server/uv.lock
generated
@@ -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" },
|
{ 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]]
|
[[package]]
|
||||||
name = "pytest-httpx"
|
name = "pytest-httpx"
|
||||||
version = "0.34.0"
|
version = "0.34.0"
|
||||||
@@ -2636,6 +2648,7 @@ dependencies = [
|
|||||||
{ name = "protobuf" },
|
{ name = "protobuf" },
|
||||||
{ name = "psycopg2-binary" },
|
{ name = "psycopg2-binary" },
|
||||||
{ name = "pydantic-settings" },
|
{ name = "pydantic-settings" },
|
||||||
|
{ name = "pytest-env" },
|
||||||
{ name = "python-jose", extra = ["cryptography"] },
|
{ name = "python-jose", extra = ["cryptography"] },
|
||||||
{ name = "python-multipart" },
|
{ name = "python-multipart" },
|
||||||
{ name = "redis" },
|
{ name = "redis" },
|
||||||
@@ -2699,6 +2712,7 @@ requires-dist = [
|
|||||||
{ name = "protobuf", specifier = ">=4.24.3" },
|
{ name = "protobuf", specifier = ">=4.24.3" },
|
||||||
{ name = "psycopg2-binary", specifier = ">=2.9.10" },
|
{ name = "psycopg2-binary", specifier = ">=2.9.10" },
|
||||||
{ name = "pydantic-settings", specifier = ">=2.0.2" },
|
{ 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-jose", extras = ["cryptography"], specifier = ">=3.3.0" },
|
||||||
{ name = "python-multipart", specifier = ">=0.0.6" },
|
{ name = "python-multipart", specifier = ">=0.0.6" },
|
||||||
{ name = "redis", specifier = ">=5.0.1" },
|
{ name = "redis", specifier = ">=5.0.1" },
|
||||||
|
|||||||
@@ -1,86 +0,0 @@
|
|||||||
# Chakra UI v3 Migration - Remaining Tasks
|
|
||||||
|
|
||||||
## Completed
|
|
||||||
|
|
||||||
- ✅ Migrated from Chakra UI v2 to v3 in package.json
|
|
||||||
- ✅ Updated theme.ts with whiteAlpha color palette and semantic tokens
|
|
||||||
- ✅ Added button recipe with fontWeight 600 and hover states
|
|
||||||
- ✅ Moved Poppins font from theme to HTML tag className
|
|
||||||
- ✅ Fixed deprecated props across all files:
|
|
||||||
- ✅ `isDisabled` → `disabled` (all occurrences fixed)
|
|
||||||
- ✅ `isChecked` → `checked` (all occurrences fixed)
|
|
||||||
- ✅ `isLoading` → `loading` (all occurrences fixed)
|
|
||||||
- ✅ `isOpen` → `open` (all occurrences fixed)
|
|
||||||
- ✅ `noOfLines` → `lineClamp` (all occurrences fixed)
|
|
||||||
- ✅ `align` → `alignItems` on Flex/Stack components (all occurrences fixed)
|
|
||||||
- ✅ `justify` → `justifyContent` on Flex/Stack components (all occurrences fixed)
|
|
||||||
|
|
||||||
## Migration Summary
|
|
||||||
|
|
||||||
### Files Modified
|
|
||||||
|
|
||||||
1. **app/(app)/rooms/page.tsx**
|
|
||||||
|
|
||||||
- Fixed: isDisabled, isChecked, align, justify on multiple components
|
|
||||||
- Updated temporary Select component props
|
|
||||||
|
|
||||||
2. **app/(app)/transcripts/fileUploadButton.tsx**
|
|
||||||
|
|
||||||
- Fixed: isDisabled → disabled
|
|
||||||
|
|
||||||
3. **app/(app)/transcripts/shareZulip.tsx**
|
|
||||||
|
|
||||||
- Fixed: isDisabled → disabled
|
|
||||||
|
|
||||||
4. **app/(app)/transcripts/shareAndPrivacy.tsx**
|
|
||||||
|
|
||||||
- Fixed: isLoading → loading, isOpen → open
|
|
||||||
- Updated temporary Select component props
|
|
||||||
|
|
||||||
5. **app/(app)/browse/page.tsx**
|
|
||||||
|
|
||||||
- Fixed: isOpen → open, align → alignItems, justify → justifyContent
|
|
||||||
|
|
||||||
6. **app/(app)/transcripts/transcriptTitle.tsx**
|
|
||||||
|
|
||||||
- Fixed: noOfLines → lineClamp
|
|
||||||
|
|
||||||
7. **app/(app)/transcripts/[transcriptId]/correct/topicHeader.tsx**
|
|
||||||
|
|
||||||
- Fixed: noOfLines → lineClamp
|
|
||||||
|
|
||||||
8. **app/lib/expandableText.tsx**
|
|
||||||
|
|
||||||
- Fixed: noOfLines → lineClamp
|
|
||||||
|
|
||||||
9. **app/[roomName]/page.tsx**
|
|
||||||
|
|
||||||
- Fixed: align → alignItems, justify → justifyContent
|
|
||||||
|
|
||||||
10. **app/lib/WherebyWebinarEmbed.tsx**
|
|
||||||
- Fixed: align → alignItems, justify → justifyContent
|
|
||||||
|
|
||||||
## Other Potential Issues
|
|
||||||
|
|
||||||
1. Check for Modal/Dialog component imports and usage (currently using temporary replacements)
|
|
||||||
2. Review Select component usage (using temporary replacements)
|
|
||||||
3. Test button hover states for whiteAlpha color palette
|
|
||||||
4. Verify all color palettes work correctly with the new semantic tokens
|
|
||||||
|
|
||||||
## Testing
|
|
||||||
|
|
||||||
After completing migrations:
|
|
||||||
|
|
||||||
1. Run `yarn dev` and check all pages
|
|
||||||
2. Test buttons with different color palettes
|
|
||||||
3. Verify disabled states work correctly
|
|
||||||
4. Check that text alignment and flex layouts are correct
|
|
||||||
5. Test modal/dialog functionality
|
|
||||||
|
|
||||||
## Next Steps
|
|
||||||
|
|
||||||
The Chakra UI v3 migration is now largely complete for deprecated props. The main remaining items are:
|
|
||||||
|
|
||||||
- Replace temporary Modal and Select components with proper Chakra v3 implementations
|
|
||||||
- Thorough testing of all UI components
|
|
||||||
- Performance optimization if needed
|
|
||||||
Reference in New Issue
Block a user