add spaced repetition system (SRS) models, engine, and word selector logic
This commit is contained in:
parent
2a1f15fdf2
commit
abe16c20ef
0
src/greek_lang/srs/__init__.py
Normal file
0
src/greek_lang/srs/__init__.py
Normal file
24
src/greek_lang/srs/engine.py
Normal file
24
src/greek_lang/srs/engine.py
Normal file
@ -0,0 +1,24 @@
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from .models import UserWordProgress, ReviewState
|
||||
|
||||
|
||||
def sm2_update(p: UserWordProgress, grade: int, now: datetime) -> None:
|
||||
# grade: 0..5
|
||||
e = p.ease + (0.1 - (5 - grade) * (0.08 + (5 - grade) * 0.02))
|
||||
p.ease = max(1.3, e)
|
||||
if grade < 3:
|
||||
p.interval_days = 1
|
||||
p.reps = 0
|
||||
p.lrn_step = 0
|
||||
p.state = ReviewState.lapsed
|
||||
else:
|
||||
if p.reps == 0:
|
||||
p.interval_days = 1
|
||||
elif p.reps == 1:
|
||||
p.interval_days = 6
|
||||
else:
|
||||
p.interval_days = round(p.interval_days * p.ease)
|
||||
p.reps += 1
|
||||
p.state = ReviewState.review
|
||||
p.due_at = now + timedelta(days=p.interval_days)
|
||||
66
src/greek_lang/srs/models.py
Normal file
66
src/greek_lang/srs/models.py
Normal file
@ -0,0 +1,66 @@
|
||||
import enum
|
||||
import datetime as dt
|
||||
|
||||
from sqlalchemy import (
|
||||
BigInteger,
|
||||
Integer,
|
||||
Float,
|
||||
DateTime,
|
||||
Enum,
|
||||
ForeignKey,
|
||||
UniqueConstraint,
|
||||
func,
|
||||
)
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from greek_lang.database.base import Base
|
||||
|
||||
|
||||
class ReviewState(enum.StrEnum):
|
||||
learning = "learning"
|
||||
review = "review"
|
||||
lapsed = "lapsed"
|
||||
|
||||
|
||||
class UserWordProgress(Base):
|
||||
__tablename__ = "srs_progress"
|
||||
id: Mapped[int] = mapped_column(BigInteger, primary_key=True)
|
||||
user_id: Mapped[int] = mapped_column(
|
||||
ForeignKey("users.id"), index=True, nullable=False
|
||||
)
|
||||
word_id: Mapped[int] = mapped_column(
|
||||
ForeignKey("glossary_word.id"), index=True, nullable=False
|
||||
)
|
||||
due_at: Mapped[dt.datetime] = mapped_column(
|
||||
DateTime(timezone=True), index=True, server_default=func.now(), nullable=False
|
||||
)
|
||||
interval_days: Mapped[int] = mapped_column(Integer, nullable=False, default=1)
|
||||
ease: Mapped[float] = mapped_column(Float, nullable=False, default=2.5)
|
||||
reps: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
|
||||
lrn_step: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
|
||||
state: Mapped[ReviewState] = mapped_column(
|
||||
Enum(ReviewState, native_enum=False),
|
||||
nullable=False,
|
||||
default=ReviewState.learning,
|
||||
)
|
||||
|
||||
__table_args__ = (UniqueConstraint("user_id", "word_id", name="uq_srs_user_word"),)
|
||||
|
||||
|
||||
class ReviewLog(Base):
|
||||
__tablename__ = "srs_review_log"
|
||||
id: Mapped[int] = mapped_column(BigInteger, primary_key=True)
|
||||
user_id: Mapped[int] = mapped_column(
|
||||
ForeignKey("users.id"), index=True, nullable=False
|
||||
)
|
||||
word_id: Mapped[int] = mapped_column(
|
||||
ForeignKey("glossary_word.id"), index=True, nullable=False
|
||||
)
|
||||
ts: Mapped[dt.datetime] = mapped_column(
|
||||
DateTime(timezone=True), server_default=func.now(), nullable=False
|
||||
)
|
||||
grade: Mapped[int] = mapped_column(Integer, nullable=False) # 0..5
|
||||
prev_interval: Mapped[int] = mapped_column(Integer, nullable=False)
|
||||
new_interval: Mapped[int] = mapped_column(Integer, nullable=False)
|
||||
prev_ease: Mapped[float] = mapped_column(Float, nullable=False)
|
||||
new_ease: Mapped[float] = mapped_column(Float, nullable=False)
|
||||
28
src/greek_lang/srs/selector.py
Normal file
28
src/greek_lang/srs/selector.py
Normal file
@ -0,0 +1,28 @@
|
||||
from collections.abc import Sequence
|
||||
|
||||
from sqlalchemy import select, func
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from greek_lang.glossaries.models import GlossaryWord
|
||||
|
||||
from .models import UserWordProgress
|
||||
|
||||
|
||||
async def pick_due_words(
|
||||
db: AsyncSession,
|
||||
user_id: int,
|
||||
limit: int = 10,
|
||||
) -> Sequence[tuple[GlossaryWord, UserWordProgress]]:
|
||||
stmt = (
|
||||
select(GlossaryWord, UserWordProgress)
|
||||
.join(UserWordProgress, UserWordProgress.word_id == GlossaryWord.id)
|
||||
.where(
|
||||
UserWordProgress.user_id == user_id,
|
||||
UserWordProgress.due_at <= func.now(),
|
||||
)
|
||||
.order_by(UserWordProgress.due_at)
|
||||
.limit(limit)
|
||||
)
|
||||
|
||||
result = await db.execute(stmt)
|
||||
return result.tuples().all()
|
||||
@ -28,7 +28,7 @@ async def translate(
|
||||
source_lang=source_lang,
|
||||
target_lang=target_lang,
|
||||
)
|
||||
pronon = await get_pronunciation(text="έμπορος", source_lang=source_lang)
|
||||
pronon = await get_pronunciation(text=word_response.lemma, source_lang=source_lang)
|
||||
|
||||
async with db_session_maker() as db_session, db_session.begin():
|
||||
values = {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user