Skip to content

Commit 0cb261f

Browse files
JSchoreelsmortii
authored andcommitted
Allow users to use Stability (FSRS) instead of Interval to determine known threshold
1 parent 6253529 commit 0cb261f

27 files changed

+239
-106
lines changed

.pre-commit-config.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ repos:
2424
docs/src/img/learn-now.mp4|
2525
docs/src/img/adding-extra-fields.mp4|
2626
test/data/card_collections/big_japanese_collection.anki2|
27+
test/data/card_collections/card_stability_collection.anki2|
28+
test/data/card_collections/card_interval_collection.anki2|
2729
test/data/am_dbs/big_japanese_collection.db|
2830
)$
2931
- id: check-case-conflict

ankimorphs/ankimorphs_config.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ class RawConfigKeys:
9797
RECALC_NUMBER_OF_MORPHS_TO_OFFSET = "recalc_number_of_morphs_to_offset"
9898
RECALC_MOVE_NEW_CARDS_TO_THE_END = "recalc_move_new_cards_to_the_end"
9999
READ_KNOWN_MORPHS_FOLDER = "read_known_morphs_folder"
100+
USE_STABILITY_FOR_KNOWN_THRESHOLD = "use_stability_for_known_threshold"
100101
TOOLBAR_STATS_USE_KNOWN = "toolbar_stats_use_known"
101102
TOOLBAR_STATS_USE_SEEN = "toolbar_stats_use_seen"
102103
EXTRA_FIELDS_DISPLAY_INFLECTIONS = "extra_fields_display_inflections"
@@ -373,6 +374,11 @@ def __init__(self, is_default: bool = False) -> None:
373374
expected_type=bool,
374375
use_default=is_default,
375376
)
377+
self.use_stability_for_known_threshold: bool = self._get_config_item(
378+
key=RawConfigKeys.USE_STABILITY_FOR_KNOWN_THRESHOLD,
379+
expected_type=bool,
380+
use_default=is_default,
381+
)
376382
self.toolbar_stats_use_known: bool = self._get_config_item(
377383
key=RawConfigKeys.TOOLBAR_STATS_USE_KNOWN,
378384
expected_type=bool,

ankimorphs/ankimorphs_globals.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
"""
66

77
# Semantic Versioning https://semver.org/
8-
__version__ = "6.0.3"
8+
__version__ = "6.1.0"
99

1010
DEV_MODE: bool = False
1111

ankimorphs/config.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,5 +92,6 @@
9292
"tag_ready": "am-ready",
9393
"tag_suspended_automatically": "am-suspended-automatically",
9494
"toolbar_stats_use_known": false,
95-
"toolbar_stats_use_seen": true
95+
"toolbar_stats_use_seen": true,
96+
"use_stability_for_known_threshold": false
9697
}

ankimorphs/recalc/anki_data_utils.py

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ class AnkiDBRowData:
2222
__slots__ = (
2323
"card_id",
2424
"card_interval",
25+
"card_stability",
2526
"card_type",
2627
"note_id",
2728
"note_fields",
@@ -35,22 +36,26 @@ def __init__(self, data_row: Sequence[Any]) -> None:
3536
assert isinstance(data_row[1], int)
3637
self.card_interval: int = data_row[1]
3738

38-
assert isinstance(data_row[2], int)
39-
self.card_type: int = data_row[2]
39+
assert isinstance(data_row[2], float)
40+
self.card_stability: float = data_row[2]
4041

41-
assert isinstance(data_row[4], int)
42-
self.note_id: int = data_row[4]
42+
assert isinstance(data_row[3], int)
43+
self.card_type: int = data_row[3]
4344

44-
assert isinstance(data_row[5], str)
45-
self.note_fields: str = data_row[5]
45+
assert isinstance(data_row[5], int)
46+
self.note_id: int = data_row[5]
4647

4748
assert isinstance(data_row[6], str)
48-
self.note_tags: str = data_row[6]
49+
self.note_fields: str = data_row[6]
50+
51+
assert isinstance(data_row[7], str)
52+
self.note_tags: str = data_row[7]
4953

5054

5155
class AnkiCardData: # pylint:disable=too-many-instance-attributes
5256
__slots__ = (
5357
"interval",
58+
"stability",
5459
"type",
5560
"expression",
5661
"automatically_known_tag",
@@ -86,6 +91,7 @@ def __init__( # pylint:disable=too-many-arguments
8691
not_ready_tag = am_config.tag_not_ready in tags_list
8792

8893
self.interval = anki_row_data.card_interval
94+
self.stability = anki_row_data.card_stability
8995
self.type = anki_row_data.card_type
9096
self.expression = expression
9197
self.automatically_known_tag = automatically_known_tag
@@ -207,7 +213,7 @@ def _get_anki_data(
207213

208214
result: list[Sequence[Any]] = mw.col.db.all(
209215
"""
210-
SELECT cards.id, cards.ivl, cards.type, cards.queue, notes.id, notes.flds, notes.tags
216+
SELECT cards.id, cards.ivl, COALESCE(json_extract(cards.data, '$.s'), 0.0), cards.type, cards.queue, notes.id, notes.flds, notes.tags
211217
FROM cards
212218
INNER JOIN notes ON
213219
cards.nid = notes.id

ankimorphs/recalc/caching.py

Lines changed: 27 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from __future__ import annotations
22

33
import csv
4+
import math
45
from pathlib import Path
56
from typing import Any
67

@@ -91,15 +92,7 @@ def cache_anki_data( # pylint:disable=too-many-locals, too-many-branches, too-m
9192
max_value=card_amount,
9293
)
9394
card_data: AnkiCardData = cards_data_dict[card_id]
94-
95-
if card_data.automatically_known_tag or card_data.manually_known_tag:
96-
highest_interval = am_config.interval_for_known_morphs
97-
elif card_data.type == 1: # 1: learning
98-
# cards in the 'learning' state have an interval of zero, but we don't
99-
# want to treat them as 'unknown', so we change the value manually.
100-
highest_interval = 1
101-
else:
102-
highest_interval = card_data.interval
95+
card_memory_strength = _get_card_memory_strength(am_config, card_data)
10396

10497
card_table_data.append(
10598
{
@@ -120,7 +113,7 @@ def cache_anki_data( # pylint:disable=too-many-locals, too-many-branches, too-m
120113
"lemma": morph.lemma,
121114
"inflection": morph.inflection,
122115
"highest_lemma_learning_interval": None, # updates later
123-
"highest_inflection_learning_interval": highest_interval,
116+
"highest_inflection_learning_interval": card_memory_strength,
124117
}
125118
)
126119
card_morph_map_table_data.append(
@@ -131,7 +124,7 @@ def cache_anki_data( # pylint:disable=too-many-locals, too-many-branches, too-m
131124
}
132125
)
133126

134-
if am_config.read_known_morphs_folder is True:
127+
if am_config.read_known_morphs_folder:
135128
progress_utils.background_update_progress(label="Importing known morphs")
136129
morph_table_data += _get_morphs_from_files(am_config)
137130

@@ -146,6 +139,29 @@ def cache_anki_data( # pylint:disable=too-many-locals, too-many-branches, too-m
146139
am_db.con.close()
147140

148141

142+
def _get_card_memory_strength(
143+
am_config: AnkiMorphsConfig, card_data: AnkiCardData
144+
) -> int:
145+
if card_data.automatically_known_tag or card_data.manually_known_tag:
146+
return am_config.interval_for_known_morphs
147+
148+
if card_data.type == 0: # 0: new
149+
# force a zero value as an early exit and to prevent edge cases.
150+
return 0
151+
152+
if card_data.type == 1: # 1: learning
153+
# cards in the 'learning' state have an interval of zero, but we don't
154+
# want to treat them as 'unknown', so we change the value manually.
155+
return 1
156+
157+
if am_config.use_stability_for_known_threshold:
158+
# Stability being a float, we floor it to get an integer value of "secured" interval, and we
159+
# give a minimum stability of 1 since the card is not new
160+
return max(1, math.floor(card_data.stability))
161+
162+
return card_data.interval
163+
164+
149165
def _get_morphs_from_files(am_config: AnkiMorphsConfig) -> list[dict[str, Any]]:
150166
assert mw is not None
151167

ankimorphs/settings/settings_general_tab.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ def __init__(
3434
self._raw_config_key_to_check_box: dict[str, QCheckBox] = {
3535
RawConfigKeys.RECALC_ON_SYNC: self.ui.recalcBeforeSyncCheckBox,
3636
RawConfigKeys.READ_KNOWN_MORPHS_FOLDER: self.ui.recalcReadKnownMorphsFolderCheckBox,
37+
RawConfigKeys.USE_STABILITY_FOR_KNOWN_THRESHOLD: self.ui.useStabilityThresholdForKnownMorphsCheckBox,
3738
RawConfigKeys.HIDE_RECALC_TOOLBAR: self.ui.hideRecalcCheckBox,
3839
RawConfigKeys.HIDE_LEMMA_TOOLBAR: self.ui.hideLemmaCheckBox,
3940
RawConfigKeys.HIDE_INFLECTION_TOOLBAR: self.ui.hideInflectionCheckBox,

ankimorphs/ui/settings_dialog.ui

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,13 @@
152152
</item>
153153
</layout>
154154
</item>
155+
<item>
156+
<widget class="QCheckBox" name="useStabilityThresholdForKnownMorphsCheckBox">
157+
<property name="text">
158+
<string>Use FSRS card stability instead of card interval for known threshold</string>
159+
</property>
160+
</widget>
161+
</item>
155162
<item>
156163
<widget class="QCheckBox" name="recalcReadKnownMorphsFolderCheckBox">
157164
<property name="text">

ankimorphs/ui/settings_dialog_ui.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,9 @@ def setupUi(self, SettingsDialog):
8080
spacerItem1 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Minimum)
8181
self.horizontalLayout_6.addItem(spacerItem1)
8282
self.verticalLayout_17.addLayout(self.horizontalLayout_6)
83+
self.useStabilityThresholdForKnownMorphsCheckBox = QtWidgets.QCheckBox(parent=self.groupBox_3)
84+
self.useStabilityThresholdForKnownMorphsCheckBox.setObjectName("useStabilityThresholdForKnownMorphsCheckBox")
85+
self.verticalLayout_17.addWidget(self.useStabilityThresholdForKnownMorphsCheckBox)
8386
self.recalcReadKnownMorphsFolderCheckBox = QtWidgets.QCheckBox(parent=self.groupBox_3)
8487
self.recalcReadKnownMorphsFolderCheckBox.setObjectName("recalcReadKnownMorphsFolderCheckBox")
8588
self.verticalLayout_17.addWidget(self.recalcReadKnownMorphsFolderCheckBox)
@@ -964,6 +967,7 @@ def retranslateUi(self, SettingsDialog):
964967
self.groupBox_3.setTitle(_translate("SettingsDialog", "Known Morphs"))
965968
self.label_16.setText(_translate("SettingsDialog", "Morphs are considered known when they have a learning interval of"))
966969
self.label_17.setText(_translate("SettingsDialog", "days or more"))
970+
self.useStabilityThresholdForKnownMorphsCheckBox.setText(_translate("SettingsDialog", "Use FSRS card stability instead of card interval for known threshold"))
967971
self.recalcReadKnownMorphsFolderCheckBox.setText(_translate("SettingsDialog", "Read files in \'known-morphs\' folder and register morphs as known"))
968972
self.groupBox_2.setTitle(_translate("SettingsDialog", "On Sync"))
969973
self.recalcBeforeSyncCheckBox.setText(_translate("SettingsDialog", "Automatically Recalc before Anki sync"))

docs/src/contributors.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ know, and I'll add you ;)
66

77
### Code contribution
88

9-
mortii, Vilhelm-Ian, xofm31, Jcuhfehl, schiozzone, Tartee, wolearyc, mdraves91, hans, RobHelgeson.
9+
mortii, Vilhelm-Ian, xofm31, Jcuhfehl, schiozzone, Tartee, wolearyc, mdraves91, hans, RobHelgeson, JSchoreels.
1010

1111
### Docs contribution
1212

0 commit comments

Comments
 (0)