| from __future__ import annotations |
|
|
| from datetime import datetime |
| from typing import Any |
|
|
| from sqlalchemy import ( |
| JSON, |
| BigInteger, |
| DateTime, |
| Float, |
| ForeignKey, |
| Integer, |
| String, |
| Text, |
| UniqueConstraint, |
| func, |
| ) |
| from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship |
|
|
| from app.core.constants import ( |
| DEFAULT_INTERFACE_LANGUAGE, |
| DEFAULT_LEVEL, |
| DEFAULT_SPEECH_RATE, |
| DEFAULT_TOPIC, |
| DEFAULT_VOICE, |
| ) |
|
|
|
|
| class Base(DeclarativeBase): |
| """Declarative base for all ORM models.""" |
|
|
|
|
| class User(Base): |
| __tablename__ = "users" |
|
|
| id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=False) |
| level: Mapped[str] = mapped_column(String(2), default=DEFAULT_LEVEL.value, nullable=False) |
| voice_name: Mapped[str] = mapped_column(String(32), default=DEFAULT_VOICE.value, nullable=False) |
| topic: Mapped[str] = mapped_column(String(32), default=DEFAULT_TOPIC.value, nullable=False) |
| speech_rate: Mapped[float] = mapped_column(Float, default=DEFAULT_SPEECH_RATE, nullable=False) |
| interface_language: Mapped[str] = mapped_column( |
| String(2), |
| default=DEFAULT_INTERFACE_LANGUAGE.value, |
| server_default=DEFAULT_INTERFACE_LANGUAGE.value, |
| nullable=False, |
| ) |
| last_assessed_at: Mapped[datetime | None] = mapped_column( |
| DateTime(timezone=True), nullable=True, default=None |
| ) |
| assessment_message_count: Mapped[int] = mapped_column(Integer, default=0, server_default="0") |
| created_at: Mapped[datetime] = mapped_column( |
| DateTime(timezone=True), server_default=func.now(), nullable=False |
| ) |
| updated_at: Mapped[datetime] = mapped_column( |
| DateTime(timezone=True), |
| server_default=func.now(), |
| onupdate=func.now(), |
| nullable=False, |
| ) |
|
|
| turns: Mapped[list[DialogTurn]] = relationship( |
| back_populates="user", cascade="all, delete-orphan", lazy="raise" |
| ) |
|
|
|
|
| class DialogTurn(Base): |
| __tablename__ = "dialog_turns" |
|
|
| id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) |
| user_id: Mapped[int] = mapped_column( |
| BigInteger, ForeignKey("users.id", ondelete="CASCADE"), index=True, nullable=False |
| ) |
| role: Mapped[str] = mapped_column(String(16), nullable=False) |
| text: Mapped[str | None] = mapped_column(Text, nullable=True) |
| corrections_count: Mapped[int] = mapped_column(Integer, default=0, server_default="0") |
| |
| audio_key: Mapped[str | None] = mapped_column(String(255), nullable=True) |
| created_at: Mapped[datetime] = mapped_column( |
| DateTime(timezone=True), server_default=func.now(), nullable=False |
| ) |
|
|
| user: Mapped[User] = relationship(back_populates="turns") |
|
|
|
|
| class SavedWord(Base): |
| __tablename__ = "saved_words" |
| __table_args__ = (UniqueConstraint("user_id", "word"),) |
|
|
| id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) |
| user_id: Mapped[int] = mapped_column( |
| BigInteger, ForeignKey("users.id", ondelete="CASCADE"), index=True, nullable=False |
| ) |
| word: Mapped[str] = mapped_column(String(100), nullable=False) |
| meaning: Mapped[str] = mapped_column(Text, nullable=False) |
| context: Mapped[str | None] = mapped_column(Text, nullable=True) |
| created_at: Mapped[datetime] = mapped_column( |
| DateTime(timezone=True), server_default=func.now(), nullable=False |
| ) |
|
|
|
|
| class PronunciationError(Base): |
| __tablename__ = "pronunciation_errors" |
| __table_args__ = (UniqueConstraint("user_id", "word_example"),) |
|
|
| id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) |
| user_id: Mapped[int] = mapped_column( |
| BigInteger, ForeignKey("users.id", ondelete="CASCADE"), index=True, nullable=False |
| ) |
| expected_phoneme: Mapped[str] = mapped_column(String(200), nullable=False) |
| actual_phoneme: Mapped[str] = mapped_column(String(200), nullable=False) |
| word_example: Mapped[str] = mapped_column(String(100), nullable=False) |
| occurrence_count: Mapped[int] = mapped_column(Integer, default=1, server_default="1") |
| last_seen_at: Mapped[datetime] = mapped_column( |
| DateTime(timezone=True), server_default=func.now(), nullable=False |
| ) |
| created_at: Mapped[datetime] = mapped_column( |
| DateTime(timezone=True), server_default=func.now(), nullable=False |
| ) |
|
|
|
|
| class GrammarExercise(Base): |
| __tablename__ = "grammar_exercises" |
| __table_args__ = (UniqueConstraint("user_id", "wrong_text", "correct_text"),) |
|
|
| id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) |
| user_id: Mapped[int] = mapped_column( |
| BigInteger, ForeignKey("users.id", ondelete="CASCADE"), index=True, nullable=False |
| ) |
| wrong_text: Mapped[str] = mapped_column(Text, nullable=False) |
| correct_text: Mapped[str] = mapped_column(Text, nullable=False) |
| rule: Mapped[str] = mapped_column(Text, nullable=False) |
| easiness_factor: Mapped[float] = mapped_column(Float, default=2.5, server_default="2.5") |
| interval: Mapped[int] = mapped_column(Integer, default=1, server_default="1") |
| repetitions: Mapped[int] = mapped_column(Integer, default=0, server_default="0") |
| next_review_at: Mapped[datetime | None] = mapped_column( |
| DateTime(timezone=True), nullable=True, default=None |
| ) |
| created_at: Mapped[datetime] = mapped_column( |
| DateTime(timezone=True), server_default=func.now(), nullable=False |
| ) |
|
|
|
|
| class LevelAssessment(Base): |
| __tablename__ = "level_assessments" |
|
|
| id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) |
| user_id: Mapped[int] = mapped_column( |
| BigInteger, ForeignKey("users.id", ondelete="CASCADE"), index=True, nullable=False |
| ) |
| level: Mapped[str] = mapped_column(String(2), nullable=False) |
| confidence: Mapped[float] = mapped_column(Float, nullable=False) |
| reasoning: Mapped[str] = mapped_column(Text, nullable=False) |
| created_at: Mapped[datetime] = mapped_column( |
| DateTime(timezone=True), server_default=func.now(), nullable=False |
| ) |
|
|
|
|
| class SpeechAssessment(Base): |
| """Per-turn speech quality scores (0-100). A null metric means it could not be measured.""" |
|
|
| __tablename__ = "speech_assessments" |
|
|
| id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) |
| user_id: Mapped[int] = mapped_column( |
| BigInteger, ForeignKey("users.id", ondelete="CASCADE"), index=True, nullable=False |
| ) |
| dialog_turn_id: Mapped[int | None] = mapped_column( |
| ForeignKey("dialog_turns.id", ondelete="SET NULL"), index=True, nullable=True |
| ) |
| accuracy: Mapped[int | None] = mapped_column(Integer, nullable=True) |
| fluency: Mapped[int | None] = mapped_column(Integer, nullable=True) |
| prosody: Mapped[int | None] = mapped_column(Integer, nullable=True) |
| vocabulary: Mapped[int | None] = mapped_column(Integer, nullable=True) |
| grammar: Mapped[int | None] = mapped_column(Integer, nullable=True) |
| topic: Mapped[int | None] = mapped_column(Integer, nullable=True) |
| transcript: Mapped[str] = mapped_column(Text, nullable=False, default="") |
| |
| corrections: Mapped[list[dict[str, Any]] | None] = mapped_column(JSON, nullable=True) |
| |
| |
| pronunciation_notes: Mapped[list[dict[str, Any]] | None] = mapped_column(JSON, nullable=True) |
| |
| stress_accuracy: Mapped[int | None] = mapped_column(Integer, nullable=True) |
| |
| |
| stress_notes: Mapped[list[dict[str, Any]] | None] = mapped_column(JSON, nullable=True) |
| |
| peaks: Mapped[list[int] | None] = mapped_column(JSON, nullable=True) |
| |
| duration_sec: Mapped[float | None] = mapped_column(Float, nullable=True) |
| created_at: Mapped[datetime] = mapped_column( |
| DateTime(timezone=True), server_default=func.now(), nullable=False |
| ) |
|
|