mirror of
https://github.com/ankitects/anki.git
synced 2025-09-18 14:02:21 -04:00
parent
9fd6d86a3e
commit
003cdfd2ec
10 changed files with 64 additions and 15 deletions
2
Cargo.lock
generated
2
Cargo.lock
generated
|
@ -1477,7 +1477,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "fsrs"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/open-spaced-repetition/fsrs-rs.git?rev=c34ac397635b6d04fdee07ace9401047f37a5f3e#c34ac397635b6d04fdee07ace9401047f37a5f3e"
|
||||
source = "git+https://github.com/open-spaced-repetition/fsrs-rs.git?rev=5089470af3944f7efbf386be09f3858342d4d5af#5089470af3944f7efbf386be09f3858342d4d5af"
|
||||
dependencies = [
|
||||
"burn",
|
||||
"itertools 0.11.0",
|
||||
|
|
|
@ -36,7 +36,7 @@ rev = "184b2ca50ed39ca43da13f0b830a463861adb9ca"
|
|||
|
||||
[workspace.dependencies.fsrs]
|
||||
git = "https://github.com/open-spaced-repetition/fsrs-rs.git"
|
||||
rev = "c34ac397635b6d04fdee07ace9401047f37a5f3e"
|
||||
rev = "5089470af3944f7efbf386be09f3858342d4d5af"
|
||||
# path = "../../../fsrs-rs"
|
||||
|
||||
[workspace.dependencies]
|
||||
|
|
|
@ -331,6 +331,7 @@ deck-config-optimize-button = Optimize
|
|||
deck-config-compute-button = Compute
|
||||
deck-config-analyze-button = Analyze
|
||||
deck-config-desired-retention = Desired retention
|
||||
deck-config-sm2-retention = SM2 retention
|
||||
deck-config-smaller-is-better = Smaller numbers indicate a better fit to your review history.
|
||||
deck-config-steps-too-large-for-fsrs = When FSRS is enabled, learning steps over 1 day are not recommended.
|
||||
deck-config-get-params = Get Params
|
||||
|
|
|
@ -142,6 +142,7 @@ message DeckConfig {
|
|||
// for fsrs
|
||||
float desired_retention = 37;
|
||||
bool reschedule_fsrs_cards = 39;
|
||||
float sm2_retention = 40;
|
||||
|
||||
bytes other = 255;
|
||||
}
|
||||
|
|
|
@ -71,6 +71,7 @@ const DEFAULT_DECK_CONFIG_INNER: DeckConfigInner = DeckConfigInner {
|
|||
desired_retention: 0.9,
|
||||
other: Vec::new(),
|
||||
reschedule_fsrs_cards: false,
|
||||
sm2_retention: 0.9,
|
||||
};
|
||||
|
||||
impl Default for DeckConfig {
|
||||
|
@ -275,6 +276,7 @@ pub(crate) fn ensure_deck_config_values_valid(config: &mut DeckConfigInner) {
|
|||
0.7,
|
||||
0.97,
|
||||
);
|
||||
ensure_f32_valid(&mut config.sm2_retention, default.sm2_retention, 0.7, 0.97)
|
||||
}
|
||||
|
||||
fn ensure_f32_valid(val: &mut f32, default: f32, min: f32, max: f32) {
|
||||
|
|
|
@ -73,6 +73,8 @@ pub struct DeckConfSchema11 {
|
|||
stop_timer_on_answer: bool,
|
||||
#[serde(default)]
|
||||
reschedule_fsrs_cards: bool,
|
||||
#[serde(default)]
|
||||
sm2_retention: f32,
|
||||
|
||||
#[serde(flatten)]
|
||||
other: HashMap<String, Value>,
|
||||
|
@ -263,6 +265,7 @@ impl Default for DeckConfSchema11 {
|
|||
fsrs_weights: vec![],
|
||||
desired_retention: 0.9,
|
||||
reschedule_fsrs_cards: false,
|
||||
sm2_retention: 0.9,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -335,6 +338,7 @@ impl From<DeckConfSchema11> for DeckConfig {
|
|||
fsrs_weights: c.fsrs_weights,
|
||||
desired_retention: c.desired_retention,
|
||||
reschedule_fsrs_cards: c.reschedule_fsrs_cards,
|
||||
sm2_retention: c.sm2_retention,
|
||||
other: other_bytes,
|
||||
},
|
||||
}
|
||||
|
@ -430,6 +434,7 @@ impl From<DeckConfig> for DeckConfSchema11 {
|
|||
fsrs_weights: i.fsrs_weights,
|
||||
desired_retention: i.desired_retention,
|
||||
reschedule_fsrs_cards: i.reschedule_fsrs_cards,
|
||||
sm2_retention: 0.9,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -454,7 +459,8 @@ static RESERVED_DECKCONF_KEYS: Set<&'static str> = phf_set! {
|
|||
"fsrsWeights",
|
||||
"desiredRetention",
|
||||
"stopTimerOnAnswer",
|
||||
"rescheduleFsrsCards"
|
||||
"rescheduleFsrsCards",
|
||||
"sm2Retention",
|
||||
};
|
||||
|
||||
static RESERVED_DECKCONF_NEW_KEYS: Set<&'static str> = phf_set! {
|
||||
|
|
|
@ -236,6 +236,7 @@ impl Collection {
|
|||
desired_retention: c.inner.desired_retention,
|
||||
max_interval: c.inner.maximum_review_interval,
|
||||
reschedule: c.inner.reschedule_fsrs_cards,
|
||||
sm2_retention: c.inner.sm2_retention,
|
||||
})
|
||||
} else {
|
||||
None
|
||||
|
|
|
@ -361,8 +361,13 @@ impl Collection {
|
|||
// and will need its initial memory state to be calculated based on review
|
||||
// history.
|
||||
let revlog = self.revlog_for_srs(SearchNode::CardIds(card.id.to_string()))?;
|
||||
let item = single_card_revlog_to_item(&fsrs, revlog, timing.next_day_at);
|
||||
card.set_memory_state(&fsrs, item);
|
||||
let item = single_card_revlog_to_item(
|
||||
&fsrs,
|
||||
revlog,
|
||||
timing.next_day_at,
|
||||
config.inner.sm2_retention,
|
||||
);
|
||||
card.set_memory_state(&fsrs, item, config.inner.sm2_retention);
|
||||
}
|
||||
let days_elapsed = self
|
||||
.storage
|
||||
|
|
|
@ -30,6 +30,7 @@ pub struct ComputeMemoryProgress {
|
|||
pub(crate) struct UpdateMemoryStateRequest {
|
||||
pub weights: Weights,
|
||||
pub desired_retention: f32,
|
||||
pub sm2_retention: f32,
|
||||
pub max_interval: u32,
|
||||
pub reschedule: bool,
|
||||
}
|
||||
|
@ -57,7 +58,13 @@ impl Collection {
|
|||
None
|
||||
};
|
||||
let fsrs = FSRS::new(req.as_ref().map(|w| &w.weights[..]).or(Some([].as_slice())))?;
|
||||
let items = fsrs_items_for_memory_state(&fsrs, revlog, timing.next_day_at);
|
||||
let sm2_retention = req.as_ref().map(|w| w.sm2_retention);
|
||||
let items = fsrs_items_for_memory_state(
|
||||
&fsrs,
|
||||
revlog,
|
||||
timing.next_day_at,
|
||||
sm2_retention.unwrap_or(0.9),
|
||||
);
|
||||
let desired_retention = req.as_ref().map(|w| w.desired_retention);
|
||||
let mut progress = self.new_progress_handler::<ComputeMemoryProgress>();
|
||||
progress.update(false, |s| s.total_cards = items.len() as u32)?;
|
||||
|
@ -66,7 +73,7 @@ impl Collection {
|
|||
let mut card = self.storage.get_card(card_id)?.or_not_found(card_id)?;
|
||||
let original = card.clone();
|
||||
if let Some(req) = &req {
|
||||
card.set_memory_state(&fsrs, item);
|
||||
card.set_memory_state(&fsrs, item, sm2_retention.unwrap());
|
||||
card.desired_retention = desired_retention;
|
||||
// if rescheduling
|
||||
if let Some(reviews) = &last_reviews {
|
||||
|
@ -127,10 +134,16 @@ impl Collection {
|
|||
.get_deck_config(conf_id)?
|
||||
.or_not_found(conf_id)?;
|
||||
let desired_retention = config.inner.desired_retention;
|
||||
let sm2_retention = config.inner.sm2_retention;
|
||||
let fsrs = FSRS::new(Some(&config.inner.fsrs_weights))?;
|
||||
let revlog = self.revlog_for_srs(SearchNode::CardIds(card.id.to_string()))?;
|
||||
let item = single_card_revlog_to_item(&fsrs, revlog, self.timing_today()?.next_day_at);
|
||||
card.set_memory_state(&fsrs, item);
|
||||
let item = single_card_revlog_to_item(
|
||||
&fsrs,
|
||||
revlog,
|
||||
self.timing_today()?.next_day_at,
|
||||
sm2_retention,
|
||||
);
|
||||
card.set_memory_state(&fsrs, item, sm2_retention);
|
||||
Ok(ComputeMemoryStateResponse {
|
||||
state: card.memory_state.map(Into::into),
|
||||
desired_retention,
|
||||
|
@ -143,6 +156,7 @@ impl Card {
|
|||
&mut self,
|
||||
fsrs: &FSRS,
|
||||
item: Option<FsrsItemWithStartingState>,
|
||||
sm2_retention: f32,
|
||||
) {
|
||||
self.memory_state = item
|
||||
.map(|i| fsrs.memory_state(i.item, i.starting_state))
|
||||
|
@ -151,7 +165,11 @@ impl Card {
|
|||
None
|
||||
} else {
|
||||
// no valid revlog entries; infer state from current card state
|
||||
Some(fsrs.memory_state_from_sm2(self.ease_factor(), self.interval as f32))
|
||||
Some(fsrs.memory_state_from_sm2(
|
||||
self.ease_factor(),
|
||||
self.interval as f32,
|
||||
sm2_retention,
|
||||
))
|
||||
}
|
||||
})
|
||||
.map(Into::into);
|
||||
|
@ -172,6 +190,7 @@ pub(crate) fn fsrs_items_for_memory_state(
|
|||
fsrs: &FSRS,
|
||||
revlogs: Vec<RevlogEntry>,
|
||||
next_day_at: TimestampSecs,
|
||||
sm2_retention: f32,
|
||||
) -> Vec<(CardId, Option<FsrsItemWithStartingState>)> {
|
||||
revlogs
|
||||
.into_iter()
|
||||
|
@ -180,7 +199,7 @@ pub(crate) fn fsrs_items_for_memory_state(
|
|||
.map(|(card_id, group)| {
|
||||
(
|
||||
card_id,
|
||||
single_card_revlog_to_item(fsrs, group.collect(), next_day_at),
|
||||
single_card_revlog_to_item(fsrs, group.collect(), next_day_at, sm2_retention),
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
|
@ -214,6 +233,7 @@ pub(crate) fn single_card_revlog_to_item(
|
|||
fsrs: &FSRS,
|
||||
entries: Vec<RevlogEntry>,
|
||||
next_day_at: TimestampSecs,
|
||||
sm2_retention: f32,
|
||||
) -> Option<FsrsItemWithStartingState> {
|
||||
let have_learning = entries
|
||||
.iter()
|
||||
|
@ -232,7 +252,7 @@ pub(crate) fn single_card_revlog_to_item(
|
|||
};
|
||||
let interval = first_review.interval.max(1);
|
||||
let starting_state =
|
||||
fsrs.memory_state_from_sm2(ease_factor as f32 / 1000.0, interval as f32);
|
||||
fsrs.memory_state_from_sm2(ease_factor as f32 / 1000.0, interval as f32, sm2_retention);
|
||||
let items = single_card_revlog_to_items(entries, next_day_at, false);
|
||||
items.and_then(|mut items| {
|
||||
let mut item = items.pop().unwrap();
|
||||
|
@ -277,6 +297,7 @@ mod tests {
|
|||
revlog(RevlogReviewKind::Review, 1),
|
||||
],
|
||||
TimestampSecs::now(),
|
||||
0.9,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
|
@ -287,7 +308,7 @@ mod tests {
|
|||
})
|
||||
);
|
||||
let mut card = Card::default();
|
||||
card.set_memory_state(&fsrs, Some(item));
|
||||
card.set_memory_state(&fsrs, Some(item), 0.9);
|
||||
assert_eq!(
|
||||
card.memory_state,
|
||||
Some(FsrsMemoryState {
|
||||
|
@ -305,12 +326,13 @@ mod tests {
|
|||
..revlog(RevlogReviewKind::Review, 100)
|
||||
}],
|
||||
TimestampSecs::now(),
|
||||
0.9,
|
||||
);
|
||||
assert!(item.is_none());
|
||||
card.interval = 123;
|
||||
card.ease_factor = 2000;
|
||||
card.ctype = CardType::Review;
|
||||
card.set_memory_state(&fsrs, item);
|
||||
card.set_memory_state(&fsrs, item, 0.9);
|
||||
assert_eq!(
|
||||
card.memory_state,
|
||||
Some(FsrsMemoryState {
|
||||
|
@ -331,7 +353,7 @@ mod tests {
|
|||
ease_factor: 1300,
|
||||
..Default::default()
|
||||
};
|
||||
card.set_memory_state(&FSRS::new(Some(&[])).unwrap(), None);
|
||||
card.set_memory_state(&FSRS::new(Some(&[])).unwrap(), None, 0.9);
|
||||
assert_eq!(
|
||||
card.memory_state,
|
||||
Some(
|
||||
|
|
|
@ -214,6 +214,17 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
</SettingTitle>
|
||||
</SpinBoxFloatRow>
|
||||
|
||||
<SpinBoxFloatRow
|
||||
bind:value={$config.sm2Retention}
|
||||
defaultValue={defaults.sm2Retention}
|
||||
min={0.7}
|
||||
max={0.97}
|
||||
>
|
||||
<SettingTitle>
|
||||
{tr.deckConfigSm2Retention()}
|
||||
</SettingTitle>
|
||||
</SpinBoxFloatRow>
|
||||
|
||||
<div class="ms-1 me-1">
|
||||
<WeightsInputRow
|
||||
bind:value={$config.fsrsWeights}
|
||||
|
|
Loading…
Reference in a new issue