For AI agents: the complete documentation index is available at https://open-spaced-repetition.github.io/ts-fsrs/llms.txt, the full documentation bundle is available at https://open-spaced-repetition.github.io/ts-fsrs/llms-full.txt, and this page is available as Markdown at https://open-spaced-repetition.github.io/ts-fsrs/guide/optimizer/train-evaluate.md.

Train and evaluate

Training runs review history through the binding in a few calls. The input is a review-log CSV; the output is a weight array you can hand to DefaultScheduler or a custom defineScheduler.

The examples import @open-spaced-repetition/binding. In Node that is the native napi module; in a browser it resolves to the WASI Preview 1 wasm build (see the Optimizer overview for threading).

Convert review logs to items

convertCsvToFsrsItems reads CSV text and groups rows by card into FSRSItems. The columns are:

ColumnMeaning
card_idStable identifier for the card.
review_timeReview time as epoch milliseconds.
review_ratingRating, 1 Again through 4 Easy.
review_durationTime spent on the review, in milliseconds.
review_stateCard state at review time.

Pass nextDayStartsAt (the hour of day the next day begins) and either an IANA timezone such as 'UTC' or a fixed UTC offset in minutes. For a Uint8Array input the call returns items synchronously:

import { convertCsvToFsrsItems } from '@open-spaced-repetition/binding'

// One card with two reviews: first review today, second the next day.
const csv = [
  'review_time,card_id,review_rating,review_duration,review_state',
  '1704067200000,card-one,3,1000,0',
  '1704153600000,card-one,4,900,2',
].join('\n')

// The day starts at 04:00 local, in the UTC timezone.
const items = convertCsvToFsrsItems(
  new TextEncoder().encode(csv),
  4,
  'UTC',
  'FSRS-7'
)

console.log(
  JSON.stringify(
    {
      itemCount: items.length,
      reviewsPerItem: items.map((item) => item.reviews.length),
      ratings: items.map((item) => item.reviews.map((review) => review.rating)),
    },
    null,
    2
  )
)

The existing converter keeps returning an array. To normalize arbitrary CSV card identifiers once, use convertCsvToFsrsItemsWithCardIds. It returns { items, cardIds }, or a Promise for a stream. The numeric IDs are assigned per retained card within this conversion, stay aligned with items after sorting, and are not the original identifiers. Reuse the same IDs for training and direct evaluation; do not combine IDs from separate conversions.

computeParameters and evaluateWithTimeSeriesSplits accept optional options.cardIds.

import {
  computeParameters,
  convertCsvToFsrsItemsWithCardIds,
  evaluateWithTimeSeriesSplits,
} from '@open-spaced-repetition/binding'

const response = await fetch(`${import.meta.env.BASE_URL}/revlog.csv`, {
  cache: 'force-cache',
})
if (!response.ok) throw new Error(`revlog.csv: ${response.status}`)
if (!response.body) throw new Error('revlog.csv: empty response body')
console.log('Response received. Converting reviews and training…')

const dataset = await convertCsvToFsrsItemsWithCardIds(
  response.body,
  4,
  'UTC',
  'FSRS-7'
)
const optimizedWeights = await computeParameters(dataset.items, {
  modelVersion: 'FSRS-7',
  enableShortTerm: true,
  cardIds: dataset.cardIds,
})
const metrics = await evaluateWithTimeSeriesSplits(dataset.items, {
  modelVersion: 'FSRS-7',
  enableShortTerm: true,
  cardIds: dataset.cardIds,
})

console.log(JSON.stringify({ optimizedWeights, metrics }, null, 2))

Use the same modelVersion ('FSRS-7' by default, or 'FSRS-6') for CSV conversion, training, evaluation, and scheduling. Pass it as the fourth argument to convertCsvToFsrsItems and in the training/evaluation options, then call DefaultScheduler({ version: modelVersion, weights }). FSRS-7 produces 34 weights and preserves fractional and same-day intervals normalized by the actual local study-day duration; FSRS-6 produces 21 weights and uses whole study days. Same-day follow-up reviews can therefore contribute to FSRS-7 training.

Train weights

computeParameters(items, options) is asynchronous and returns the trained weights. enableShortTerm is required; numRelearningSteps and trainingConfig are optional, and progress reports (current, total) while training runs:

const weights = await computeParameters(items, {
  modelVersion: 'FSRS-7',
  enableShortTerm: true,
  enableSchedPenalties: true,
})

enableSchedPenalties is optional and defaults to false. Set it to true to add FSRS-7 interval-growth and short-interval penalties to the training loss. It also applies to training inside evaluateWithTimeSeriesSplits. It is ignored by FSRS-6, and does not change scheduling formulas.

trainingConfig (epochs, batch size, seed, max sequence length, learning rate, and L2 weight) is optional and falls back to the binding's defaults. computeParameters runs the real training loop, so call it in a Worker rather than on the main thread. The trainer on the Playground page runs this same path in a Web Worker and reports progress live. The returned weights array is what you pass to DefaultScheduler({ version: modelVersion, weights }) or a defineScheduler config, replacing the model defaults.

Evaluate fit

evaluateWithTimeSeriesSplits(items, options) splits the review history by time and returns a ModelEvaluation with logLoss and rmseBins, letting you judge whether the trained weights predict held-out reviews. computeOptimalSteps goes further: given the reviews and a target retention, it recommends learning and relearning step durations.

See the Optimizer overview for the two runtimes and install commands.