-
Notifications
You must be signed in to change notification settings - Fork 0
feat(meeting): implement real-time websocket gateway for audio, signa… #47
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
de6a4b5
a46f693
589e4eb
933993f
99fecef
ee83dc7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,39 @@ | ||
| """Add user_role column | ||
|
|
||
| Revision ID: 4e7d4d5e7661 | ||
| Revises: a37ad6ed5842 | ||
| Create Date: 2026-04-06 17:07:47.505824 | ||
|
|
||
| """ | ||
|
|
||
| from typing import Sequence, Union | ||
|
|
||
| from alembic import op | ||
| import sqlalchemy as sa | ||
|
|
||
| # revision identifiers, used by Alembic. | ||
| revision: str = "4e7d4d5e7661" | ||
| down_revision: Union[str, Sequence[str], None] = "a37ad6ed5842" | ||
Check noticeCode scanning / CodeQL Unused global variable Note
The global variable 'down_revision' is not used.
|
||
|
|
||
| branch_labels: Union[str, Sequence[str], None] = None | ||
|
aniebietafia marked this conversation as resolved.
Dismissed
|
||
| depends_on: Union[str, Sequence[str], None] = None | ||
|
aniebietafia marked this conversation as resolved.
Dismissed
|
||
|
|
||
|
|
||
| def upgrade() -> None: | ||
| """Upgrade schema.""" | ||
| # ### commands auto generated by Alembic - please adjust! ### | ||
| op.add_column( | ||
| "users", | ||
| sa.Column( | ||
| "user_role", sa.String(length=50), server_default="user", nullable=False | ||
| ), | ||
| ) | ||
| op.create_index(op.f("ix_users_user_role"), "users", ["user_role"], unique=False) | ||
| # ### end Alembic commands ### | ||
|
|
||
|
|
||
| def downgrade() -> None: | ||
| """Downgrade schema.""" | ||
| # ### commands auto generated by Alembic - please adjust! ### | ||
| op.drop_index(op.f("ix_users_user_role"), table_name="users") | ||
| op.drop_column("users", "user_role") | ||
| # ### end Alembic commands ### | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| import enum | ||
|
|
||
|
|
||
| class UserRole(enum.StrEnum): | ||
| ADMIN = "admin" | ||
| USER = "user" | ||
|
|
||
|
|
||
| class SupportedLanguage(enum.StrEnum): | ||
| ENGLISH = "en" | ||
| FRENCH = "fr" | ||
| GERMAN = "de" | ||
| SPANISH = "es" | ||
| ITALIAN = "it" | ||
| PORTUGUESE = "pt" |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,6 +4,7 @@ | |
| from sqlalchemy import Boolean, DateTime, ForeignKey, String | ||
| from sqlalchemy.orm import Mapped, mapped_column | ||
|
|
||
| from app.auth.constants import UserRole | ||
| from app.models.base import Base | ||
|
|
||
|
|
||
|
|
@@ -41,6 +42,14 @@ class User(Base): | |
| speaking_language: Mapped[str] = mapped_column(String(10), default="en") | ||
| listening_language: Mapped[str] = mapped_column(String(10), default="en") | ||
|
|
||
| # Role | ||
| user_role: Mapped[str] = mapped_column( | ||
| String(50), | ||
| default=UserRole.USER.value, | ||
| server_default=UserRole.USER.value, | ||
| index=True, | ||
| ) | ||
|
Comment on lines
+46
to
+51
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# Verify whether role constraints already exist in model + migration.
set -euo pipefail
echo "== Model definition =="
rg -n -C3 "user_role|CheckConstraint|Enum\\(" app/auth/models.py
echo
echo "== Migration(s) touching user_role =="
fd -i "add_user_role_column" alembic/versions || true
rg -n -C3 "user_role|CHECK|Enum|create_check_constraint|sa\\.Enum" alembic/versionsRepository: Brints/FluentMeet Length of output: 2185 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "== UserRole enum definition =="
fd "\.py$" app/auth | xargs rg -l "UserRole" | head -5
echo
echo "== Searching for UserRole enum =="
rg -n "class UserRole" -A 10 app/
echo
echo "== Checking for validators in User model =="
rg -n "@validator|@field_validator|def __init__|def validate" app/auth/models.py -A 3
echo
echo "== Full User model to check for constraints =="
head -60 app/auth/models.pyRepository: Brints/FluentMeet Length of output: 683 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "== Searching for schema validation (Pydantic/Marshmallow) =="
find app/auth -name "*.py" -exec grep -l "schema\|Schema\|Pydantic\|BaseModel" {} \;
echo
echo "== Checking app/auth directory structure =="
ls -la app/auth/
echo
echo "== Searching for user_role in routes/services =="
rg -n "user_role" app/ --type py -B 2 -A 2 | head -50
echo
echo "== Check if User model has __table_args__ =="
rg -n "__table_args__" app/auth/models.py -A 5Repository: Brints/FluentMeet Length of output: 2485 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "== Full User model =="
cat app/auth/models.py
echo
echo "== Service layer user_role operations =="
rg -n "user_role\s*=" app/auth/service.py -B 3 -A 3Repository: Brints/FluentMeet Length of output: 2982 Constrain
This allows arbitrary strings to be written to the database, bypassing the 🔒 Suggested direction (DB constraint)-from sqlalchemy import Boolean, DateTime, ForeignKey, String
+from sqlalchemy import Boolean, CheckConstraint, DateTime, ForeignKey, String
@@
class User(Base):
__tablename__ = "users"
+ __table_args__ = (
+ CheckConstraint(
+ "user_role IN ('user', 'admin')",
+ name="ck_users_user_role",
+ ),
+ )🤖 Prompt for AI Agents |
||
|
|
||
|
|
||
| def default_expiry() -> datetime: | ||
| return datetime.now(UTC) + timedelta(hours=24) | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,45 @@ | ||
| import logging | ||
|
|
||
| from sqlalchemy import select | ||
| from sqlalchemy.orm import Session | ||
|
|
||
| from app.auth.constants import UserRole | ||
| from app.auth.models import User | ||
| from app.core.config import settings | ||
| from app.core.security import security_service | ||
|
|
||
| logger = logging.getLogger(__name__) | ||
|
|
||
|
|
||
| def init_admin(db: Session) -> None: | ||
| if not settings.ADMIN_EMAIL or not settings.ADMIN_PASSWORD: | ||
| logger.info( | ||
| "Admin credentials not fully set in .env, skipping admin initialization." | ||
| ) | ||
| return | ||
|
|
||
| admin_email = settings.ADMIN_EMAIL.lower() | ||
|
|
||
| stmt = select(User).where(User.email == admin_email) | ||
| existing_admin = db.execute(stmt).scalar_one_or_none() | ||
|
|
||
| if existing_admin: | ||
| if existing_admin.user_role != UserRole.ADMIN.value: | ||
| existing_admin.user_role = UserRole.ADMIN.value | ||
| db.commit() | ||
| logger.info("Existing admin user updated with ADMIN role.") | ||
| return | ||
|
|
||
| logger.info("Creating default admin user: System Admin") | ||
|
|
||
| admin_user = User( | ||
| email=admin_email, | ||
| full_name="System Admin", | ||
| hashed_password=security_service.hash_password(settings.ADMIN_PASSWORD), | ||
| user_role=UserRole.ADMIN.value, | ||
| is_active=True, | ||
| is_verified=True, | ||
| ) | ||
| db.add(admin_user) | ||
| db.commit() | ||
| logger.info("Default admin user created successfully.") |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| from app.external_services.deepgram.service import DeepgramSTTService | ||
|
|
||
| __all__ = ["DeepgramSTTService"] |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| """Deepgram provider configuration.""" | ||
|
|
||
| from app.core.config import settings | ||
|
|
||
|
|
||
| def get_deepgram_headers() -> dict[str, str]: | ||
| """Return authorization headers for the Deepgram REST API.""" | ||
| if not settings.DEEPGRAM_API_KEY: | ||
| raise RuntimeError("DEEPGRAM_API_KEY is not configured.") | ||
| return { | ||
| "Authorization": f"Token {settings.DEEPGRAM_API_KEY}", | ||
| "Content-Type": "audio/raw", | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,94 @@ | ||
| """Deepgram Speech-to-Text service. | ||
|
|
||
| Wraps the Deepgram REST API (/v1/listen) for pre-recorded audio | ||
| transcription. Each call sends a single audio chunk and returns | ||
| the transcribed text with confidence and detected language. | ||
| """ | ||
|
|
||
| import logging | ||
| import time | ||
|
|
||
| import httpx | ||
|
|
||
| from app.core.config import settings | ||
| from app.external_services.deepgram.config import get_deepgram_headers | ||
|
|
||
| logger = logging.getLogger(__name__) | ||
|
|
||
|
|
||
| class DeepgramSTTService: | ||
| """Stateless service for converting audio bytes to text via Deepgram.""" | ||
|
|
||
| def __init__(self, timeout: float = 10.0) -> None: | ||
| self._timeout = timeout | ||
|
|
||
| async def transcribe( | ||
| self, | ||
| audio_bytes: bytes, | ||
| *, | ||
| language: str = "en", | ||
| sample_rate: int = 16000, | ||
| encoding: str = "linear16", | ||
| ) -> dict: | ||
| """Send raw audio to Deepgram and return transcription results. | ||
|
|
||
| Args: | ||
| audio_bytes: Raw audio data (PCM or Opus). | ||
| language: ISO 639-1 language hint for the STT model. | ||
| sample_rate: Audio sample rate in Hz. | ||
| encoding: Audio encoding format (``linear16`` or ``opus``). | ||
|
|
||
| Returns: | ||
| A dict with keys ``text``, ``confidence``, ``detected_language``. | ||
|
|
||
| Raises: | ||
| httpx.HTTPStatusError: On non-2xx responses from Deepgram. | ||
| """ | ||
| headers = get_deepgram_headers() | ||
| params = { | ||
| "model": settings.DEEPGRAM_MODEL, | ||
| "language": language, | ||
| "encoding": encoding, | ||
| "sample_rate": str(sample_rate), | ||
| "punctuate": "true", | ||
| "smart_format": "true", | ||
| } | ||
|
|
||
| start = time.monotonic() | ||
| async with httpx.AsyncClient(timeout=self._timeout) as client: | ||
| response = await client.post( | ||
| settings.DEEPGRAM_API_URL, | ||
| headers=headers, | ||
| params=params, | ||
| content=audio_bytes, | ||
| ) | ||
| response.raise_for_status() | ||
|
|
||
| elapsed_ms = (time.monotonic() - start) * 1000 | ||
| logger.debug("Deepgram STT completed in %.1fms", elapsed_ms) | ||
|
|
||
| data = response.json() | ||
| # Deepgram response structure: | ||
| # results.channels[0].alternatives[0].transcript | ||
| channel = data.get("results", {}).get("channels", [{}])[0] | ||
| alternative = channel.get("alternatives", [{}])[0] | ||
|
|
||
| return { | ||
| "text": alternative.get("transcript", ""), | ||
| "confidence": alternative.get("confidence", 0.0), | ||
| "detected_language": data.get("results", {}).get( | ||
| "detected_language", language | ||
| ), | ||
| "latency_ms": round(elapsed_ms, 1), | ||
| } | ||
|
|
||
|
|
||
| # ── Module-level singleton ──────────────────────────────────────────── | ||
| _stt_service: DeepgramSTTService | None = None | ||
|
|
||
|
|
||
| def get_deepgram_stt_service() -> DeepgramSTTService: | ||
| global _stt_service # noqa: PLW0603 | ||
| if _stt_service is None: | ||
| _stt_service = DeepgramSTTService() | ||
| return _stt_service |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| from app.external_services.deepl.service import DeepLTranslationService | ||
|
|
||
| __all__ = ["DeepLTranslationService"] |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| """DeepL provider configuration.""" | ||
|
|
||
| from app.core.config import settings | ||
|
|
||
|
|
||
| def get_deepl_headers() -> dict[str, str]: | ||
| """Return authorization headers for the DeepL REST API.""" | ||
| if not settings.DEEPL_API_KEY: | ||
| raise RuntimeError("DEEPL_API_KEY is not configured.") | ||
| return { | ||
| "Authorization": f"DeepL-Auth-Key {settings.DEEPL_API_KEY}", | ||
| "Content-Type": "application/json", | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.