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,
|
source_lang=source_lang,
|
||||||
target_lang=target_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():
|
async with db_session_maker() as db_session, db_session.begin():
|
||||||
values = {
|
values = {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user