An open, decentralization-friendly flashcard deck format.
Current draft: mflash v6. The reference implementation is the Rust module
schema/mflash_v6.rs; the normative contract for
non-Rust implementations is the JSON Schema generated from it. Everything
older (v1–v5) is kept for history and migration — see
Legacy versions.
- Decentralization & torrent friendly. A deck is a plain directory
(
deck.json+manifest.json+media/) you can commit to git or seed as a torrent. Media is content-addressed by SHA-256 in the manifest, so identical files dedupe across decks, chunks verify independently, and nothing central is required. Decks carry UUIDs, an SPDXlicensefield, andforked_fromlineage so they can be shared, remixed, and re-shared. - Polyglot by default. A deck declares
default_term_langanddefault_def_lang(BCP-47). Any individual piece of text can override its language by writing{ "value": "chien", "lang": "fr" }instead of a bare string — one field, per card, per example, per translation. - Image occlusion (and five other card kinds) built in.
basic,cloze(Anki-compatible{{c1::…}}markers),image_occlusion(rect / ellipse / polygon masks in normalized 0–1 coordinates, so masks survive resizing),multiple_choice,listening, andmedia_prompt. - Content and study state never mix. Review history lives in a separate
.progressfile with an append-only review log, so studying never rewrites the shared, torrented, version-controlled deck — and any scheduler (SM-2, FSRS, Leitner) can rebuild its state from the raw log.
deck.json:
{
"format": "mflash",
"version": 6,
"id": "0b2e7c1a-3f4d-4e8a-9c6b-2d1f0a9e5b77",
"title": "Japanese Core",
"default_term_lang": "ja",
"default_def_lang": "en",
"cards": [
{ "id": "5d9c2a44-7e0f-4b6e-8b1a-c3d2e1f0a9b8",
"content": { "basic": { "term": "犬", "definition": "dog" } } },
{ "id": "9e8d7c6b-5a49-4382-9170-ffeeddccbbaa",
"content": { "basic": {
"term": "猫",
"definition": { "value": "chat", "lang": "fr" } } } }
]
}The second card overrides just its definition language — that is the whole polyglot override mechanism.
Every learner-facing string is a Text. Resolution is a two-step chain —
own tag first, then the deck default for that field's side:
flowchart LR
T["Text field<br/>(term, definition, hint, …)"] --> Q{"has its own<br/>lang tag?"}
Q -- "yes — value + lang object" --> OWN["use it<br/>(per-card override)"]
Q -- "no — bare string" --> SIDE{"which side is<br/>the field on?"}
SIDE -- "term side:<br/>term · cloze text · transcript<br/>prompt · occlusion answer" --> TL["deck.default_term_lang"]
SIDE -- "definition side:<br/>definition · translations<br/>answer · hint · notes" --> DL["deck.default_def_lang"]
style OWN fill:#d5e8d4,stroke:#82b366
style TL fill:#dae8fc,stroke:#6c8ebf
style DL fill:#dae8fc,stroke:#6c8ebf
One envelope (identity, tags, attachments) around exactly one typed payload:
flowchart TD
C["Card"] --> ENV["envelope<br/>id · tags · media · extensions"]
C --> CT["content — exactly one of:"]
CT --> B["basic<br/>term / definition"]
CT --> CL["cloze<br/>Anki-style cN markers"]
CT --> IO["image_occlusion<br/>masks over an image"]
CT --> MC["multiple_choice<br/>1+ correct choices"]
CT --> LI["listening<br/>audio → transcript"]
CT --> MP["media_prompt<br/>media → free answer"]
style C fill:#ffe6cc,stroke:#d79b00
style ENV fill:#f5f5f5,stroke:#666666
style IO fill:#d5e8d4,stroke:#82b366
Occlusion masks use normalized 0–1 coordinates (rect, ellipse, or polygon), so they survive any image resize. Review history keys on stable per-mask and per-choice ids.
Authoring / git / torrent source is a loose directory:
Japanese/
├── deck.json one Deck (canonical JSON; RON supported as an editing view)
├── manifest.json logical path → { sha256, bytes, mime }
└── media/
├── inu.mp3 human-readable names while authoring
└── cell.png
Japanese.mflash (a ZIP with media STORED uncompressed, files named by their
SHA-256) is an export/transport artifact only, produced by pack and
consumed by unpack. Study progress lives next to the package in a
.progress file, never inside it.
| You are writing… | Start here |
|---|---|
| A reader/writer in any language | schema/mflash-v6.schema.json (deck), …-manifest, …-progress |
| Rust | schema/mflash_v6.rs — serde types, Deck::validate(), Deck::validate_against(&Manifest), round-trip tests |
| A test suite | examples/v6-kitchen-sink/ — every card kind, polyglot overrides, a real content-addressed manifest |
The Rust types are the reference implementation; the generated JSON Schemas are the normative contract. They cannot drift: CI regenerates the schemas from the types and fails on any diff.
cargo test # reference impl + example guard
cargo run --features jsonschema --bin gen-schema # regenerate normative schemas
python scripts/validate.py <deck.json> schema/mflash-v6.schema.jsonReader rules (details in the module docs of schema/mflash_v6.rs):
- Ignore unknown fields (minor revisions add fields).
- Refuse decks whose
min_reader_versionexceeds what you implement, with a clear "needs a newer reader" message (new card kinds are major-version events). - Treat
.mflashand the loose directory as equivalent after loading. - Never write scheduling state into
deck.json.
schema/ mflash_v6.rs (reference impl) + generated v6 JSON Schemas + legacy schemas
specs/ prose specs (v1–v4 history; v6 is specified by schema/ + module docs)
examples/ example decks; v6-kitchen-sink/ is the canonical one
scripts/ Python validators and v1→v2→v3 migrators
docs/ decision records and diagrams
| Version | Form | Status |
|---|---|---|
| v6 | Rust reference + generated JSON Schema | current draft |
| v5 | schema/mflash-v5.proto |
superseded design study (protobuf) |
| v4 | specs/mflash-v4.md |
superseded draft |
| v3 | specs/mflash-v3.md, schema/mflash-v3-schema.json |
legacy, examples retained |
| v2 | specs/mflash-v2.md, schema/mflash-v2.schema.json |
legacy stable |
| v1 | specs/mflash-v1.md, schema/mflash-v1.schema.json |
legacy |
Migration notes: MIGRATION.md. Design history:
docs/decisions/, CHANGELOG.md.
Public domain (Unlicense). Decks themselves declare their own
license via the SPDX license field.
