feat: database models and Alembic migrations — User, Recording, Transcription, Protocol, PromptTemplate, ExportFile

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-03 22:03:20 +05:00
parent 296538b0ea
commit 5b92553148
14 changed files with 344 additions and 0 deletions
View File
+28
View File
@@ -0,0 +1,28 @@
from pydantic_settings import BaseSettings
class Settings(BaseSettings):
database_url: str = "postgresql+asyncpg://meetproto:changeme@db:5432/meetproto"
redis_url: str = "redis://redis:6379/0"
jwt_secret: str = "change-this-to-a-random-secret"
jwt_algorithm: str = "HS256"
jwt_expire_minutes: int = 1440
whisper_model: str = "large-v3"
whisper_device: str = "cuda"
whisper_compute_type: str = "float16"
litellm_model: str = "openrouter/meta-llama/llama-3.1-70b-instruct"
litellm_api_key: str = ""
litellm_api_base: str = ""
upload_dir: str = "/app/uploads"
export_dir: str = "/app/exports"
retention_days: int = 90
telegram_bot_token: str = ""
telegram_webhook_url: str = ""
app_host: str = "0.0.0.0"
app_port: int = 8000
app_url: str = "http://localhost:8000"
model_config = {"env_file": ".env", "extra": "ignore"}
settings = Settings()
+11
View File
@@ -0,0 +1,11 @@
from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine
from app.config import settings
engine = create_async_engine(settings.database_url, echo=False)
async_session = async_sessionmaker(engine, expire_on_commit=False)
async def get_db():
async with async_session() as session:
yield session
+23
View File
@@ -0,0 +1,23 @@
from app.models.base import Base
from app.models.export_file import ExportFile, ExportFormat
from app.models.prompt_template import PromptTemplate, TemplateType
from app.models.protocol import Protocol, ProtocolStatus
from app.models.recording import Recording, RecordingSource, RecordingStatus
from app.models.transcription import Transcription
from app.models.user import User, UserRole
__all__ = [
"Base",
"User",
"UserRole",
"Recording",
"RecordingSource",
"RecordingStatus",
"Transcription",
"Protocol",
"ProtocolStatus",
"PromptTemplate",
"TemplateType",
"ExportFile",
"ExportFormat",
]
+21
View File
@@ -0,0 +1,21 @@
import uuid
from datetime import datetime
from sqlalchemy import DateTime, func
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
class Base(DeclarativeBase):
pass
class TimestampMixin:
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), server_default=func.now()
)
class UUIDMixin:
id: Mapped[uuid.UUID] = mapped_column(
primary_key=True, default=uuid.uuid4
)
+26
View File
@@ -0,0 +1,26 @@
import enum
import uuid
from sqlalchemy import BigInteger, Enum, ForeignKey, String
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.models.base import Base, TimestampMixin, UUIDMixin
class ExportFormat(str, enum.Enum):
docx = "docx"
pdf = "pdf"
md = "md"
class ExportFile(UUIDMixin, TimestampMixin, Base):
__tablename__ = "export_files"
protocol_id: Mapped[uuid.UUID] = mapped_column(
ForeignKey("protocols.id", ondelete="CASCADE")
)
format: Mapped[ExportFormat] = mapped_column(Enum(ExportFormat))
file_path: Mapped[str] = mapped_column(String(1000))
file_size: Mapped[int] = mapped_column(BigInteger)
protocol: Mapped["Protocol"] = relationship(back_populates="export_files")
+28
View File
@@ -0,0 +1,28 @@
import enum
from sqlalchemy import Boolean, Enum, String, Text
from sqlalchemy.dialects.postgresql import JSONB
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.models.base import Base, TimestampMixin, UUIDMixin
class TemplateType(str, enum.Enum):
client_survey = "client_survey"
client_intro = "client_intro"
internal = "internal"
custom = "custom"
class PromptTemplate(UUIDMixin, TimestampMixin, Base):
__tablename__ = "prompt_templates"
name: Mapped[str] = mapped_column(String(255))
description: Mapped[str] = mapped_column(String(1000), default="")
type: Mapped[TemplateType] = mapped_column(Enum(TemplateType))
system_prompt: Mapped[str] = mapped_column(Text)
user_prompt: Mapped[str] = mapped_column(Text)
output_schema: Mapped[dict] = mapped_column(JSONB, default=dict)
is_default: Mapped[bool] = mapped_column(Boolean, default=False)
protocols: Mapped[list["Protocol"]] = relationship(back_populates="template")
+39
View File
@@ -0,0 +1,39 @@
import enum
import uuid
from datetime import datetime
from sqlalchemy import DateTime, Enum, ForeignKey, String, Text, func
from sqlalchemy.dialects.postgresql import JSONB
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.models.base import Base, TimestampMixin, UUIDMixin
class ProtocolStatus(str, enum.Enum):
generating = "generating"
done = "done"
error = "error"
class Protocol(UUIDMixin, TimestampMixin, Base):
__tablename__ = "protocols"
transcription_id: Mapped[uuid.UUID] = mapped_column(
ForeignKey("transcriptions.id", ondelete="CASCADE")
)
template_id: Mapped[uuid.UUID] = mapped_column(
ForeignKey("prompt_templates.id")
)
content: Mapped[dict] = mapped_column(JSONB, default=dict)
raw_text: Mapped[str] = mapped_column(Text, default="")
llm_model: Mapped[str] = mapped_column(String(100))
status: Mapped[ProtocolStatus] = mapped_column(
Enum(ProtocolStatus), default=ProtocolStatus.generating
)
updated_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), server_default=func.now(), onupdate=func.now()
)
transcription: Mapped["Transcription"] = relationship(back_populates="protocols")
template: Mapped["PromptTemplate"] = relationship(back_populates="protocols")
export_files: Mapped[list["ExportFile"]] = relationship(back_populates="protocol")
+40
View File
@@ -0,0 +1,40 @@
import enum
import uuid
from sqlalchemy import BigInteger, Enum, ForeignKey, Interval, String
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.models.base import Base, TimestampMixin, UUIDMixin
class RecordingSource(str, enum.Enum):
web = "web"
telegram = "telegram"
lensa = "lensa"
class RecordingStatus(str, enum.Enum):
uploaded = "uploaded"
processing = "processing"
done = "done"
error = "error"
class Recording(UUIDMixin, TimestampMixin, Base):
__tablename__ = "recordings"
user_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("users.id"))
title: Mapped[str] = mapped_column(String(500))
file_path: Mapped[str] = mapped_column(String(1000))
file_size: Mapped[int] = mapped_column(BigInteger)
duration: Mapped[float | None] = mapped_column(nullable=True)
format: Mapped[str] = mapped_column(String(20))
source: Mapped[RecordingSource] = mapped_column(Enum(RecordingSource))
status: Mapped[RecordingStatus] = mapped_column(
Enum(RecordingStatus), default=RecordingStatus.uploaded
)
user: Mapped["User"] = relationship(back_populates="recordings")
transcription: Mapped["Transcription | None"] = relationship(
back_populates="recording", uselist=False
)
+23
View File
@@ -0,0 +1,23 @@
import uuid
from sqlalchemy import ForeignKey, Interval, String, Text
from sqlalchemy.dialects.postgresql import JSONB
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.models.base import Base, TimestampMixin, UUIDMixin
class Transcription(UUIDMixin, TimestampMixin, Base):
__tablename__ = "transcriptions"
recording_id: Mapped[uuid.UUID] = mapped_column(
ForeignKey("recordings.id", ondelete="CASCADE"), unique=True
)
text: Mapped[str] = mapped_column(Text)
segments: Mapped[dict] = mapped_column(JSONB, default=list)
language: Mapped[str | None] = mapped_column(String(10), nullable=True)
whisper_model: Mapped[str] = mapped_column(String(50))
processing_time: Mapped[float | None] = mapped_column(nullable=True)
recording: Mapped["Recording"] = relationship(back_populates="transcription")
protocols: Mapped[list["Protocol"]] = relationship(back_populates="transcription")
+25
View File
@@ -0,0 +1,25 @@
import enum
from sqlalchemy import Boolean, Enum, String
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.models.base import Base, TimestampMixin, UUIDMixin
class UserRole(str, enum.Enum):
admin = "admin"
user = "user"
class User(UUIDMixin, TimestampMixin, Base):
__tablename__ = "users"
username: Mapped[str] = mapped_column(String(150), unique=True, index=True)
email: Mapped[str] = mapped_column(String(255), unique=True, index=True)
password_hash: Mapped[str] = mapped_column(String(255))
role: Mapped[UserRole] = mapped_column(
Enum(UserRole), default=UserRole.user
)
is_active: Mapped[bool] = mapped_column(Boolean, default=True)
recordings: Mapped[list["Recording"]] = relationship(back_populates="user")