askesis
Docs Make cards with AI

Docs / Your cards

Make cards with AI

A copy-paste prompt that has ChatGPT, Claude or Gemini write a deck you can import, and the exact card file spec for agents and scripts (/llms.txt).

An AI assistant can turn a topic, your notes or a whole chapter into a deck in a minute. The prompt below tells it how to write good cards and exactly how to format the file, so it imports cleanly.

Make a deck with a chat assistant

  1. Copy the prompt and paste it into ChatGPT, Claude, Gemini or any assistant you use.
  2. Fill in the lines in square brackets: the topic (or attach your notes or a document), how many cards, your level, the deck name and the language.
  3. Download the askesis-cards.csv it gives you. If it only shows the text, copy it into a file with that name.
  4. On the Decks page, choose Import and pick the file. Check the table of cards, then import. Decks with the same name are merged, never wiped (how merging works).
make-a-deck-prompt.txt
Make flashcards for Askesis, a spaced-repetition app, and give them to me as a CSV file I can import.

What I want:
- Topic or source: [a topic, or paste or attach your notes, a chapter or a document]
- Number of cards: [for example 40]
- My level: [beginner, intermediate or advanced]
- Deck: [for example Biology::Cells; "::" separates a deck from its subdecks]
- Language of the cards: [for example English]

How to write the cards:
- One fact per card. Split lists and long explanations into several cards.
- Ask a precise question with exactly one right answer, and make every card understandable on its own.
- Keep the back short: a word, a phrase or one sentence. Put context, an example or a memory aid in "extra".
- Use "hint" only for a small nudge shown under the question, such as a reading or a category. Usually leave it empty.
- Cover what matters most at my level, not trivia. No two cards may have the same front.
- For key terms in a sentence, a cloze card works well: write {{c1::the hidden part}} in the front and leave the back empty. {{c1::answer::hint}} shows the hint in the gap; c1, c2 and so on make one card per gap.
- If you're working from my material, stick to what it says and don't invent facts.

The file format (follow it exactly):
1. Start with these five lines:
#separator:comma
#html:false
#columns:deck,front,back,hint,extra,tags
#deck column:1
#tags column:6
2. Then one card per line, with six fields in this order: deck, front, back, hint, extra, tags.
3. Wrap every field in double quotes. Inside a field, write a double quote as two double quotes ("").
4. deck: the full deck path with :: between levels, the same on every row unless I asked for subdecks.
5. tags: lowercase words separated by spaces, with no spaces inside a tag (use - instead). Nest a tag with ::, like biology::cells.
6. Leave a field as "" when it has nothing. Never use HTML.
7. Math: \( … \) inside a line and \[ … \] on its own, with single backslashes (\frac, not \\frac).
8. Code: a fenced block with the language name, like ```python on its own line, then the code, then ```, all inside the quoted field.

Example:
#separator:comma
#html:false
#columns:deck,front,back,hint,extra,tags
#deck column:1
#tags column:6
"Biology::Cells","What does the mitochondrion produce?","ATP","","Cellular respiration happens here, which is why it's called the cell's power plant.","biology::cells organelles"
"Biology::Cells","{{c1::Ribosomes}} build proteins.","","","They sit free in the cytoplasm and on the rough endoplasmic reticulum.","biology::cells"
"Math::Calculus","Derivative of \(x^2\)?","\(2x\)","","Power rule: \(\frac{d}{dx} x^n = n x^{n-1}\)","calculus"

Before you answer, check that every row has exactly six fields and that there are as many cards as I asked for.

Give me the CSV as a downloadable file named askesis-cards.csv. If you can't create files, put the whole CSV in a single code block and nothing else.

I'll import it on the Decks page in Askesis (Import).

For agents and scripts

The rest of this page is written for language models and scripts that create Askesis cards. It states every rule the importer and the card renderer follow, with limits and examples.

  • Point an agent at /llms.txt: the same spec as raw Markdown.
  • Or copy the spec below into your prompt.

A short prompt for an agent that can read the web:

agent-prompt.txt
Follow the Askesis card file spec at <site>/llms.txt exactly.
Create 40 cards about <topic> for a <level> learner.
Use the deck path "<Subject>::<Topic>" and tag each card with <tags>.
One fact per card. Use \( … \) for math and fenced code blocks with a language for code.
Output only the CSV file, starting with the five header lines.

The spec

askesis-card-spec.md
# Askesis card files: a spec for AI agents

Askesis is an open-source spaced-repetition app with SM-2 and FSRS scheduling. This
document tells you exactly how to produce files that import cleanly and cards
that render correctly. Follow it literally.

## 1. Quick rules (checklist)

1. Output a UTF-8 CSV file. Start with these five header lines, exactly:

       #separator:comma
       #html:false
       #columns:deck,front,back,hint,extra,tags
       #deck column:1
       #tags column:6

2. Then one card per line with 6 fields: deck, front, back, hint, extra, tags.
3. Quote a field with double quotes if it contains a comma, a double quote, a
   line break, or leading/trailing spaces. Inside quotes, write a double quote
   as two double quotes (""). Quoting every field is always valid and is the
   safest choice. Watch out: LaTeX often contains commas (\, thin space,
   f(x, y), \{1, 2\}) and so do lists and code.
4. Deck = full path with "::" between levels, e.g. Medical::Cells.
5. Tags = space-separated words, no spaces inside a tag, e.g. "cell biology" is two tags.
   Nest tags with "::", e.g. biology::cells::organelles.
6. Front is required. Keep each card to one fact (minimum information principle).
7. Math: \( … \) inline, \[ … \] display. Code: fenced blocks with a language.
8. Backslashes are literal in CSV. Write \frac, not \\frac.
9. Never emit HTML. Use the light markup in section 6.

## 2. File structure

- Encoding: UTF-8. A leading byte-order mark is allowed and ignored.
- Line endings: LF or CRLF.
- Optional **header lines** come first, one per line, each "#key:value".
  Parsing of header lines stops at the first line that isn't one.
- Then data rows (RFC 4180 style): fields separated by the separator; a field
  may be wrapped in double quotes; inside a quoted field, "" is a literal
  quote and line breaks are allowed. Blank lines are ignored.

### Header lines

| Line | Values | Meaning |
|---|---|---|
| #separator:NAME | comma, tab, semicolon, pipe (or the literal character) | Field separator. If absent, it's detected from the first lines. |
| #html:BOOL | true, false | true = fields are HTML and get converted to plain text (section 8). Use false. |
| #columns:a,b,c | column names (section 3) | Names each column, in order. |
| #deck column:N | 1-based number | Column holding the deck path. |
| #tags column:N | 1-based number | Column holding space-separated tags. |

These are the header lines common flashcard apps use for text import and
export, so files move between those apps and Askesis.

Without #columns, a first data row made of known column names (e.g.
"Front,Back,Tags") is treated as a header row. With no names at all, column 1
is the front and column 2 is the back.

## 3. Columns

| Column | Accepted names | Required | Max length | Notes |
|---|---|---|---|---|
| deck | deck | no | 400 chars | Deck path, "Parent::Child::Grandchild". Empty = the import's default deck. |
| front | front, question, term, word | **yes** | 20000 | The prompt. Rows with an empty front are skipped. |
| back | back, answer, definition, meaning | no | 20000 | The answer. |
| hint | hint, reading | no | 2000 | Small line under the front (e.g. a reading). |
| extra | extra, notes, note | no | 20000 | Shown under the answer: context, mnemonics, sources. |
| tags | tags, tag | no | 50 tags × 200 chars | Separated by spaces. "parent::child" nests a tag under another. |

Column names are case-insensitive. Unknown columns are ignored.
Optional progress and history columns (for backups) are listed in section 7. Exports from other apps are covered in sections 8–9.

Tags nest: a card tagged "lang::spanish::verbs" is found when studying
or filtering by "lang" or "lang::spanish" (matching ignores case), and renaming or
deleting a parent tag also renames or removes the tags below it.

## 4. Decks and subdecks (grouping)

- Decks nest: "Medical::Cells", "Medical::Plants::Trees".
- Every level in a path is created if missing. Each level's name: 1–80 chars, no "::".
- Matching is by full path, **case-insensitive**, with spaces around "::" and repeated
  spaces ignored: "medical :: cells" matches "Medical::Cells".
- Importing into an existing deck **merges**: cards are added; nothing is ever deleted.
- Studying a parent deck includes all its subdecks.

## 5. Duplicates and updates

A duplicate is a card in the **same deck** whose front matches after trimming
and collapsing whitespace (case-sensitive). The importer offers three modes:

| Mode | What happens to a duplicate |
|---|---|
| update (default) | back is replaced; hint and extra are replaced only if the file has those columns; tags are merged; review history is kept; progress and history from the file follow section 7 |
| skip | left alone (the file's progress and history are ignored too) |
| duplicate | imported as an additional card |

Rows inside one file that share a deck and front are treated the same way
(the later row updates the earlier one in update mode). To update a deck
safely, re-export it, edit, and re-import with mode "update".

## 6. Card text syntax

Fields are plain text with light markup. HTML is never rendered.

### Math (KaTeX, MathJax-style delimiters)

| Write | Renders as |
|---|---|
| \( e^{i\pi} + 1 = 0 \) | inline math |
| \[ \int_0^1 x^2\,dx = \tfrac{1}{3} \] | display math (own line, centered) |
| $a^2 + b^2 = c^2$ | inline math |
| $$\sum_{k=1}^{n} k = \frac{n(n+1)}{2}$$ | display math |

- Prefer \( … \) and \[ … \]. They're unambiguous and the most portable.
- A single $…$ is math only when there's no space just inside the dollars and
  the closing $ isn't followed by a digit, so "$5 and $10" stays text.
  Write \$ for a literal dollar sign.
- KaTeX supports most of LaTeX math mode: \frac, \sqrt, \sum, \int, \lim,
  \mathbf, \mathrm, \text, \begin{pmatrix}…\end{pmatrix}, \begin{aligned}…\end{aligned}.
- Not supported: chemistry packages (\ce, mhchem), \usepackage, text-mode
  environments, macros you define. For chemistry write \mathrm{H_2O},
  \mathrm{CO_2}, \rightarrow, \rightleftharpoons.
- Invalid LaTeX is shown as its source, so the card still imports.

### Code

Fenced block, with an optional language after the opening fence:

    ```python
    squares = [n * n for n in range(10)]
    ```

- Highlighted languages: bash (sh, shell), c, cpp (c++), csharp (cs), css, dart, dockerfile (docker), elixir (ex, exs), go, graphql, haskell (hs), hcl, java, javascript (js), json, kotlin (kt), markdown (md), php, python (py), redis, ruby (rb), rust (rs), sql, swift, typescript (ts), vim, x86asm, xml/html, yaml (yml).
  Without a language the renderer guesses from that list.
- Inline code: single backticks, e.g. `Vec<T>`. Text inside backticks is
  literal (no math, no bold).
- Inside a CSV field, a fenced block needs real line breaks, so the field
  must be quoted (and code usually contains commas anyway).

### Text styling

| Write | Result |
|---|---|
| **bold** | bold |
| *italic* or _italic_ | italic |
| a line break | kept as a line break |

## 7. Progress and history columns (optional: for backups and migrations)

Append these columns, in this order, after tags: state, due, interval, ease, stability, difficulty, step, reps, lapses, last_review, suspended, history.
The #columns line then reads:

    #columns:deck,front,back,hint,extra,tags,state,due,interval,ease,stability,difficulty,step,reps,lapses,last_review,suspended,history

Leave all of them empty (or leave the columns out) for a new, unstudied card.
When you generate new cards, don't add them at all.

| Column | Type | Meaning |
|---|---|---|
| state | 0, 1, 2 or 3 | 0 new, 1 learning, 2 review, 3 relearning |
| due | ISO 8601 date-time | when the card is next due, e.g. 2026-10-16T04:00:00.000Z |
| interval | integer days, 0–36500 | current interval (0 while learning) |
| ease | number, e.g. 2.5 | SM-2 ease factor (2.5 = 250%); 0 if never graduated |
| stability | number ≥ 0 | FSRS memory stability (days); 0 if unknown |
| difficulty | number 0–10 | FSRS difficulty; 0 if unknown |
| step | integer 0–100 or empty | position in the learning or relearning steps (0 = first step); empty = 0 |
| reps | integer ≥ 0 | times reviewed |
| lapses | integer ≥ 0 | times forgotten |
| last_review | ISO 8601 date-time or empty | last answer |
| suspended | 1 or empty | 1 = suspended (excluded from study) |
| history | text, see below | past answers, oldest first; empty = none |

A row is treated as scheduled only if state is 0–3 and due is a valid date.
The history column is read either way.

### History format

Past answers, oldest first, separated by ";". Each answer is up to eight
space-separated values. Only the first two (time and rating) are required:

    <time> <rating> <ms> <state before> <state after> <interval before> <interval after> <ease after>

| Value | Type | Meaning |
|---|---|---|
| time | ISO 8601 date-time | when the answer was given |
| rating | 1, 2, 3 or 4 | 1 Again, 2 Hard, 3 Good, 4 Easy |
| ms | integer 0–3600000 | time spent on the card, in milliseconds |
| state before, state after | 0, 1, 2 or 3 | the card's state before and after the answer (as in the state column) |
| interval before, interval after | integer days, 0–36500 | the card's interval before and after the answer |
| ease after | number 0–10 | ease factor after the answer (2.5 = 250%); 0 if unknown |

Missing values default to 0, except the two states, which default to 2
(review). An answer with an invalid time or rating is skipped. Up to
5,000 answers per card. Example: a new card answered Good twice,
ten minutes apart, graduating to a 1-day interval:

    2026-03-01T09:12:00Z 3 5200 0 1 0 0 0;2026-03-01T09:22:40Z 3 3100 1 2 0 1 2.5

The field contains no commas, so it needs no quotes in a comma-separated
file. In a semicolon-separated file it must be quoted.

### How progress and history import

- Cards that don't exist yet are added with the file's progress. Cards that
  are new in the file (state 0, or no progress) stay new.
- For an existing card (same deck and front, update mode), the file's
  progress is applied only when its last_review is newer than the card's own,
  so restoring an old backup never rolls back newer study.
- History is merged into the review log without duplicates: an answer for
  the same card at the same timestamp (to the millisecond) is skipped, so
  importing a backup twice is safe.
- Imported history restores the heatmap, streaks, XP, trophies and stats.
  Imported answers can't be undone in a study session.
- The import result reports "Progress kept for N cards" and "N past reviews
  restored".

## 8. Plain-text files from other apps (.txt)

A plain-text notes export (.txt) from another flashcard app is a tab-separated
file that imports directly. It may start with header lines like:

    #separator:tab
    #html:true
    #guid column:1
    #notetype column:2
    #deck column:3
    #tags column:6

- The GUID and note type columns are skipped. The first two remaining fields
  become front and back; a third field (e.g. Basic's "Back Extra") becomes extra.
- **Cloze notes** become one card per deletion: {{c1::answer}} or
  {{c1::answer::hint}}. The front hides that deletion as […] (or [hint]) and
  shows the others; the back shows the answer in bold. The note's second field
  goes to extra.
- To export for another app, choose "Plain text (.txt)" in Askesis: a
  tab-separated file with #columns, #deck column and #tags column, which apps
  that read these header lines can import.
- .apkg and .colpkg packages can't be imported directly yet. Convert them
  with the script in the appendix (keeps each card's progress, and turns
  the package's review log into the history column; step is left empty).

## 9. HTML input (from other apps' exports)

With #html:true, each field is converted to plain text: <br>, </div>, </p>
and </li> become line breaks; <li> becomes "• "; <b>/<strong> become **bold**;
<i>/<em> become *italic*; [sound:…] and <img> references are dropped
(media isn't imported); the [latex], [$] and [$$] tags some apps use become
math: [$]…[/$] becomes \( … \), and [$$]…[/$$] and [latex]…[/latex] become
\[ … \]; entities (&amp;, &#39;, …) are decoded; all other tags are removed.

## 10. Limits

- Up to 50,000 cards per import request (the web importer splits bigger files).
- Field lengths as in section 3. Tags: up to 50 per card, 200 chars each.
- History: up to 5,000 past answers per card.

## 11. Writing good cards (what to generate)

- One fact per card; short fronts that have exactly one right answer.
- Put the cue on the front and the answer on the back; use hint for readings
  or categories and extra for context or a mnemonic.
- Prefer several small cards over one list card ("Name all 7 …").
- Use subdecks for topics (Medical::Cells) and tags for cross-cutting labels (exam-1, hard).
- Don't create duplicates of the same front inside one deck.

## 12. Examples

### Vocabulary with readings, nested decks and tags

    #separator:comma
    #html:false
    #columns:deck,front,back,hint,extra,tags
    #deck column:1
    #tags column:6
    Japanese::Kanji,脳,brain,のう,On: ノウ,kanji n3 body
    Japanese::Vocabulary,覚える,to remember; to memorize,おぼえる,,verbs n4
    Medical::Cells,What organelle makes most of a cell's ATP?,The mitochondrion,,,cell-biology
    Medical::Plants,Where does photosynthesis happen?,In the chloroplasts,,,plants

### Math, code and quoting

    #separator:comma
    #html:false
    #columns:deck,front,back,hint,extra,tags
    #deck column:1
    #tags column:6
    Math::Calculus,Derivative of \(\sin x\)?,\(\cos x\),,,derivatives
    Math::Calculus,State the power rule.,"\[\frac{d}{dx}x^n = n\,x^{n-1}\]",,Holds for any real n.,derivatives
    "Programming::Python","What does this print?
    ```python
    print([n * n for n in range(4)])
    ```","`[0, 1, 4, 9]`",,,comprehensions
    Physics,"Who said ""Imagination is more important than knowledge""?",Albert Einstein,,,quotes

### Tab-separated, no header lines (front, back)

    Mitochondrion	Powerhouse of the cell
    Ribosome	Builds proteins

### Backup with progress and history

Row 1 is a review card with its full history, row 2 is in its learning steps
(step 1), row 3 is new, and row 4 is a suspended FSRS card whose history only
gives time and rating.

    #separator:comma
    #html:false
    #columns:deck,front,back,hint,extra,tags,state,due,interval,ease,stability,difficulty,step,reps,lapses,last_review,suspended,history
    #deck column:1
    #tags column:6
    Spanish::Verbs,aprovechar,to make the most of,,,verbs b2,2,2026-03-05T09:30:00.000Z,3,2.5,0,0,0,3,0,2026-03-02T09:30:00.000Z,,2026-03-01T09:12:00Z 3 5200 0 1 0 0 0;2026-03-01T09:22:40Z 3 3100 1 2 0 1 2.5;2026-03-02T09:30:00Z 3 2800 2 2 1 3 2.5
    Spanish::Verbs,acertar,to get right; to guess correctly,,,verbs,1,2026-03-02T09:50:00.000Z,0,0,0,0,1,1,0,2026-03-02T09:40:00.000Z,,2026-03-02T09:40:00Z 3 4100 0 1 0 0 0
    Spanish::Verbs,soler,to usually do something,,,verbs,,,,,,,,,,,,
    Medical::Cells,What organelle makes most of a cell's ATP?,The mitochondrion,,,cell-biology,2,2026-05-20T08:00:00.000Z,30,2.5,31.2,5.1,0,6,1,2026-04-20T08:00:00.000Z,1,2026-03-20T08:00:00Z 1;2026-04-20T08:00:00Z 3

## 13. Defaults you can rely on

New decks use the account's default preset, which starts with these standard
values: learning steps 1m 10m, graduating interval 1 day,
easy interval 4 days, 20 new cards per day, 200 reviews per day.

## Appendix: converting flashcard packages (.apkg / .colpkg)

Askesis can't read these packages directly yet. To convert one, run the script
below with the package (or give this section and the file to an AI that can run
Python). It writes one row per card with progress and review history,
following the rules above.

I'm attaching a flashcard package (.apkg or .colpkg). Convert it to an Askesis CSV
file I can import, keeping each card's study progress and review history. Use
Python. Run the script below as-is (install the "zstandard" package if it's
missing), then give me the resulting askesis-import.csv to download. If something
fails, explain the error and fix the script. Don't invent or rewrite any card
content.

Format rules (the Askesis spec, also at /llms.txt on the Askesis site):
- One row per CARD (not per note), so every card keeps its own progress.
- Header lines first, then rows with columns:
  deck,front,back,hint,extra,tags,state,due,interval,ease,stability,difficulty,step,reps,lapses,last_review,suspended,history
- Deck paths use "::" between levels. Tags are space-separated.
- Fields keep the package's HTML; "#html:true" tells Askesis to convert it.
- Progress: state 0 new, 1 learning, 2 review, 3 relearning; due and
  last_review as ISO 8601 UTC; interval in days; ease as a factor (2.5 = 250%);
  stability/difficulty from FSRS when present; step is left empty (Askesis restarts
  the current learning steps); leave progress empty for new cards so they stay new.
- history: every past answer from the review log (revlog table), oldest first,
  separated by ";". Each answer is "time rating ms state_before state_after
  interval_before interval_after ease" (time ISO 8601 UTC, rating 1-4, intervals
  in days). Manual reschedules (revlog type 4 and 5, or ease 0) are not answers
  and are skipped.
- Cloze cards are expanded here (front hides the deletion, back shows it in bold).
- Media (images, audio) can't be imported; references are dropped by Askesis.

```python
import csv, io, json, re, sqlite3, sys, tempfile, zipfile
from datetime import datetime, timezone

PATH = sys.argv[1] if len(sys.argv) > 1 else "deck.apkg"  # set to the uploaded file
OUT = "askesis-import.csv"

def open_collection(path):
    z = zipfile.ZipFile(path)
    tmp = tempfile.NamedTemporaryFile(suffix=".sqlite", delete=False)
    name = max(n for n in z.namelist() if n.startswith("collection."))   # newest format in the package
    raw = z.read(name)
    if name.endswith("b"):                     # modern packages: zstd-compressed SQLite
        import zstandard
        raw = zstandard.ZstdDecompressor().decompressobj().decompress(raw)
    tmp.write(raw); tmp.close()
    return sqlite3.connect(tmp.name)

db = open_collection(PATH)
crt = db.execute("select crt from col").fetchone()[0]          # collection day zero, seconds

try:                                                            # schema 18: decks table, "\x1f" between levels
    decks = {i: n.replace("\x1f", "::") for i, n in db.execute("select id, name from decks")}
except sqlite3.OperationalError:                                # schema 11: JSON in col.decks
    decks = {int(k): v["name"] for k, v in json.loads(db.execute("select decks from col").fetchone()[0]).items()}

notes = {nid: (flds.split("\x1f"), tags.strip()) for nid, flds, tags in db.execute("select id, flds, tags from notes")}
last = dict(db.execute("select cid, max(id) from revlog group by cid"))   # ms timestamps

# Review history. revlog type: 0 learn, 1 review, 2 relearn, 3 filtered, 4 manual, 5 rescheduled.
# ivl/lastIvl: positive = days, negative = seconds (still in learning steps).
history = {}
for rid, cid, ease, ivl, last_ivl, factor, taken, rtype in db.execute(
        "select id, cid, ease, ivl, lastIvl, factor, time, type from revlog order by id"):
    if ease not in (1, 2, 3, 4) or rtype not in (0, 1, 2, 3):
        continue
    past = history.setdefault(cid, [])
    if rtype == 0:
        before = 1 if past else 0                  # the first learning answer is the new card's
    elif rtype == 2:
        before = 3
    elif rtype == 1:
        before = 2
    else:                                          # filtered deck: judge by the interval before
        before = 2 if last_ivl > 0 else (1 if past else 0)
    if ivl > 0:
        after = 2
    elif rtype == 3:
        after = before if before != 0 else 1
    else:
        after = 1 if rtype == 0 else 3
    past.append(" ".join(str(v) for v in (
        datetime.fromtimestamp(rid / 1000, timezone.utc).isoformat(timespec="milliseconds").replace("+00:00", "Z"),
        ease, max(0, min(taken, 3_600_000)), before, after, max(last_ivl, 0), max(ivl, 0), round(factor / 1000, 3))))

CLOZE = re.compile(r"\{\{c(\d+)::(.*?)(?:::(.*?))?\}\}", re.S)
def cloze(text, n):
    front = CLOZE.sub(lambda m: "[%s]" % ((m.group(3) or "…").strip()) if int(m.group(1)) == n else m.group(2), text)
    back = CLOZE.sub(lambda m: "<b>%s</b>" % m.group(2) if int(m.group(1)) == n else m.group(2), text)
    return front, back

def iso(seconds):
    return datetime.fromtimestamp(seconds, timezone.utc).isoformat().replace("+00:00", "Z")

rows = []
query = "select id, nid, did, ord, type, queue, due, ivl, factor, reps, lapses, odid, odue, data from cards order by nid, ord"
for cid, nid, did, ord_, ctype, queue, due, ivl, factor, reps, lapses, odid, odue, data in db.execute(query):
    fields, tags = notes[nid]
    if odid:                                   # card sits in a filtered deck: use its home deck and due
        did, due = odid, odue
    f = lambda i: fields[i] if i < len(fields) else ""
    if CLOZE.search(f(0)):                     # Cloze: one card per deletion (ord 0 = c1)
        front, back = cloze(f(0), ord_ + 1)
        extra = f(1)
    elif ord_ == 1 and len(fields) >= 2:       # "Basic (and reversed card)": the reverse card
        front, back, extra = f(1), f(0), "<br>".join(fields[2:])
    else:
        front, back, extra = f(0), f(1), "<br>".join(fields[2:])

    if ctype in (1, 2, 3):
        if ctype == 2 or queue == 3 or due < 1_000_000_000:   # day-based due (days since crt)
            due_at = crt + due * 86400
        else:                                                  # intraday learning: epoch seconds
            due_at = due
        try:
            fsrs = json.loads(data or "{}")
        except ValueError:
            fsrs = {}
        progress = [
            ctype, iso(due_at), max(ivl, 0), round(factor / 1000, 3),
            fsrs.get("s", 0) or 0, fsrs.get("d", 0) or 0, "", reps, lapses,
            iso(last[cid] / 1000) if cid in last else "",
        ]
    else:
        progress = [""] * 10
    suspended = 1 if queue == -1 else ""
    rows.append([decks.get(did, "Imported"), front, back, "", extra, tags, *progress, suspended, ";".join(history.get(cid, []))])

with open(OUT, "w", newline="", encoding="utf-8") as out:
    out.write("#separator:comma\n#html:true\n")
    out.write("#columns:deck,front,back,hint,extra,tags,state,due,interval,ease,stability,difficulty,step,reps,lapses,last_review,suspended,history\n")
    out.write("#deck column:1\n#tags column:6\n")
    csv.writer(out, quoting=csv.QUOTE_ALL).writerows(rows)

reviews = sum(len(h) for h in history.values())
print(f"Wrote {len(rows)} cards ({reviews} past reviews) from {len(notes)} notes in {len(set(r[0] for r in rows))} decks to {OUT}")
```

When it's done, tell me how many cards, past reviews and decks were converted.