diff --git a/proto/anki/scheduler.proto b/proto/anki/scheduler.proto index 4a9dbc012..536118c69 100644 --- a/proto/anki/scheduler.proto +++ b/proto/anki/scheduler.proto @@ -52,6 +52,7 @@ service SchedulerService { rpc ComputeOptimalRetention(ComputeOptimalRetentionRequest) returns (ComputeOptimalRetentionResponse); rpc EvaluateWeights(EvaluateWeightsRequest) returns (EvaluateWeightsResponse); + rpc ComputeMemoryState(cards.CardId) returns (ComputeMemoryStateResponse); } // Implicitly includes any of the above methods that are not listed in the @@ -383,3 +384,8 @@ message EvaluateWeightsResponse { float log_loss = 1; float rmse_bins = 2; } + +message ComputeMemoryStateResponse { + optional cards.FsrsMemoryState state = 1; + float desired_retention = 2; +} diff --git a/pylib/anki/collection.py b/pylib/anki/collection.py index 25b69fb16..ee5c0492b 100644 --- a/pylib/anki/collection.py +++ b/pylib/anki/collection.py @@ -127,6 +127,13 @@ class CardIdsLimit: ExportLimit = Union[DeckIdLimit, NoteIdsLimit, CardIdsLimit, None] +@dataclass +class ComputedMemoryState: + desired_retention: float + stability: float | None = None + difficulty: float | None = None + + @dataclass class AddNoteRequest: note: Note @@ -1320,6 +1327,17 @@ class Collection(DeprecatedNamesMixin): def extract_cloze_for_typing(self, text: str, ordinal: int) -> str: return self._backend.extract_cloze_for_typing(text=text, ordinal=ordinal) + def compute_memory_state(self, card_id: CardId) -> ComputedMemoryState: + resp = self._backend.compute_memory_state(card_id) + if resp.HasField("state"): + return ComputedMemoryState( + desired_retention=resp.desired_retention, + stability=resp.state.stability, + difficulty=resp.state.difficulty, + ) + else: + return ComputedMemoryState(desired_retention=resp.desired_retention) + # Timeboxing ########################################################################## # fixme: there doesn't seem to be a good reason why this code is in main.py diff --git a/rslib/src/scheduler/fsrs/memory_state.rs b/rslib/src/scheduler/fsrs/memory_state.rs index 3e9134d9f..dd4a1ffc8 100644 --- a/rslib/src/scheduler/fsrs/memory_state.rs +++ b/rslib/src/scheduler/fsrs/memory_state.rs @@ -1,10 +1,13 @@ // Copyright: Ankitects Pty Ltd and contributors // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html +use anki_proto::scheduler::ComputeMemoryStateResponse; use fsrs::FSRS; +use crate::card::FsrsMemoryState; use crate::prelude::*; use crate::scheduler::fsrs::weights::fsrs_items_for_memory_state; +use crate::scheduler::fsrs::weights::single_card_revlog_to_items; use crate::scheduler::fsrs::weights::Weights; use crate::search::JoinSearches; use crate::search::Negated; @@ -57,4 +60,33 @@ impl Collection { } Ok(()) } + + pub fn compute_memory_state(&mut self, card_id: CardId) -> Result { + let card = self.storage.get_card(card_id)?.or_not_found(card_id)?; + let deck_id = card.original_deck_id.or(card.deck_id); + let deck = self.get_deck(deck_id)?.or_not_found(card.deck_id)?; + let conf_id = DeckConfigId(deck.normal()?.config_id); + let config = self + .storage + .get_deck_config(conf_id)? + .or_not_found(conf_id)?; + let desired_retention = config.inner.desired_retention; + let fsrs = FSRS::new(Some(&config.inner.fsrs_weights))?; + let revlog = self.revlog_for_srs(SearchNode::CardIds(card.id.to_string()))?; + let items = single_card_revlog_to_items(revlog, self.timing_today()?.next_day_at, false); + if let Some(mut items) = items { + if let Some(last) = items.pop() { + let state = fsrs.memory_state(last); + let state = FsrsMemoryState::from(state); + return Ok(ComputeMemoryStateResponse { + state: Some(state.into()), + desired_retention, + }); + } + } + Ok(ComputeMemoryStateResponse { + state: None, + desired_retention, + }) + } } diff --git a/rslib/src/scheduler/fsrs/weights.rs b/rslib/src/scheduler/fsrs/weights.rs index 47a4e410f..843813582 100644 --- a/rslib/src/scheduler/fsrs/weights.rs +++ b/rslib/src/scheduler/fsrs/weights.rs @@ -130,7 +130,7 @@ pub(crate) fn fsrs_items_for_memory_state( /// `[1,2,3]`, we create FSRSItems corresponding to `[1,2]` and `[1,2,3]` /// in training, and `[1]`, [1,2]` and `[1,2,3]` when calculating memory /// state. -fn single_card_revlog_to_items( +pub(crate) fn single_card_revlog_to_items( mut entries: Vec, next_day_at: TimestampSecs, training: bool, diff --git a/rslib/src/scheduler/service/mod.rs b/rslib/src/scheduler/service/mod.rs index 8e535633d..23c7155f8 100644 --- a/rslib/src/scheduler/service/mod.rs +++ b/rslib/src/scheduler/service/mod.rs @@ -4,8 +4,10 @@ mod answering; mod states; +use anki_proto::cards; use anki_proto::generic; use anki_proto::scheduler; +use anki_proto::scheduler::ComputeMemoryStateResponse; use anki_proto::scheduler::ComputeOptimalRetentionRequest; use anki_proto::scheduler::ComputeOptimalRetentionResponse; use anki_proto::scheduler::GetOptimalRetentionParametersResponse; @@ -277,4 +279,8 @@ impl crate::services::SchedulerService for Collection { params: Some(params), }) } + + fn compute_memory_state(&mut self, input: cards::CardId) -> Result { + self.compute_memory_state(input.into()) + } }