Skip to main content

memi-engine

CI PyPI version Python versions License: MIT Ruff

In a world where a language model will answer almost anything in an instant, the part of your mind that recalls — that retrieves what you know on its own — gets little exercise. memi is a small counterweight: a game built on active recall. You look at an image, try to name it before revealing the answer, and follow the know more link to learn more. Each round strengthens the link between what you see and what you know. The answer is always one tap away — the point is to reach for it yourself first.

memi-engine lets you build your own memi game — a tap-to-reveal flashcard trainer — from a list of names and where to find their images.

You define categories (countries, animals, monuments, movies…); the engine gives you the responsive web UI, the menu, image fetching from Wikipedia and friends, filters, clue mode, a "know more" link to each item's Wikipedia (or source) page on reveal, theming, and a reporting system.

pip install memi-engine
from memi_engine import CategoryProvider, MemiConfig, create_app, register


class Animals(CategoryProvider):
    key = "nature:animals"
    items = ["Lion", "Tiger", "Elephant", "Aardvark"]


register(Animals())
app = create_app(MemiConfig(title="My Memi"))

if __name__ == "__main__":
    app.run(debug=True)

Open http://localhost:5000, pick nature → animals, and play. Images are resolved from Wikipedia automatically from each item's name.

Concepts

A memi game is just a set of category providers registered with the engine. Each provider declares:

  • items — the list of names to guess.
  • key — where the category sits in the menu (see below).
  • how to get an image for an item (default: Wikipedia), and optionally a tag (a subtitle shown on reveal) and a clue.

The engine handles routing, the random game loop (/api/random), filtering, prefetching, and rendering.

Keys are the menu

A category key is a colon-separated path. The engine splits it to build a nested menu, and renders each segment verbatim as the on-screen label — so the key is also your menu copy. This is why localized games keep their keys in the game's language:

Key Menu shown to the player
"space" space
"nature:animals" nature → animals
"nature:plants:flowers" nature → plants → flowers
"geografia:freguesias" geografia → freguesias

Up to four levels are supported. A child labelled all always sorts first.

CategoryProvider

Subclass it and set at least key and items. Override the methods you need.

class Monuments(CategoryProvider):
    key = "culture:monuments"
    items = ["Belém Tower", "Eiffel Tower"]
    override_name = True            # show the item name, not the article title

    def get_tag(self, item):       # subtitle on the revealed card
        return PARISHES.get(item)

Register each provider with register(Monuments()), or use @register as a class decorator on the definition.

Attributes

Attribute Default Meaning
key "" Menu path (see above).
items [] List of item names.
filters {} {filter_name: {value: [items]}} — auto-generates filter UI.
single_select False Only one subcategory active at a time.
light_bg False Light card background (good for logos).
override_name False Use the item key as the display name, not the article title.
footers [] Footer IDs (attribution) to show when this category is active.
tag_style None "plain", "scientific", or None (auto-detect) — tag styling.
submenu_default False Opening its submenu starts this category (and lists it first).
map_zoom None Zoom of the details panel's map (enriched layout); None = no map.
map_bounds None ((lat_min, lat_max), (lon_min, lon_max)) — no map outside it.

Methods

Method Returns
get_image(item) {"name": ..., "image": ..., "url": ...} or None. Default: Wikipedia.
get_tag(item) A short subtitle for the revealed card, or None.
get_clue(item) A clue shown before reveal, or None.
get_details(item) {"text", "area": {"name", "url"}, "map": {"lat", "lon", "zoom"}} for the enriched layout's panel, or None. Default: the Wikipedia article's intro (+ map if map_zoom).
get_wikipedia_title(item) (lang, title) of the article the panel describes. Default: from the url of get_image.
get_audio(item) {"text", "lang", "src"} for a speaker button on reveal (add "before_reveal": True to show it before), or None.

The details panel. With MemiConfig(layout="enriched") a panel opens beside the card on reveal. By default it holds the opening paragraph of the card's Wikipedia article (the one know more links to), in the game's html_lang when that edition has the article. Set map_zoom on categories of places to pin a map on the article's coordinates; override get_wikipedia_title for cards whose link isn't a Wikipedia article, or get_details to write your own.

The optional url in the get_image result is the item's source page; the engine turns it into the "know more" link shown on reveal (label set via MemiConfig.label_more). The built-in image helpers populate it automatically — e.g. get_wikipedia_image returns the Wikipedia article URL — so Wikipedia-backed categories get the link for free.

Scientific names

ScientificNameProvider tags each item with its Latin name. It ships a bundled English database (SCIENTIFIC_NAMES, ~1500 species) used by default; pass your own mapping for other languages. The tag is shown only when the Latin name differs from the display name, in italic scientific style.

from memi_engine import ScientificNameProvider, register

class Animals(ScientificNameProvider):
    key = "nature:animals"
    items = ["Lion", "Tiger"]      # → "Panthera leo", "Panthera tigris"

class Plantas(ScientificNameProvider):
    key = "natureza:plantas"
    items = ["Sobreiro"]
    scientific_names = {"Sobreiro": "Quercus suber"}

Filters

A filter maps option values to subsets of items. The engine renders the filter buttons and applies the choice via a URL parameter.

class Countries(CategoryProvider):
    key = "geography:countries"
    items = ["France", "Spain", "Japan"]
    filters = {
        "continent": {"europe": ["France", "Spain"], "asia": ["Japan"]},
    }

Images

memi_engine.images resolves item names to image URLs and caches results in-memory. Providers call these from get_image:

get_wikipedia_image, get_wikipedia_file_image, get_commons_file_image, get_tmdb_image, get_tmdb_tv_image, get_fandom_image, get_country_shape, get_album_cover, get_logo_image, and more.

Some sources need configuration via environment variables:

Variable Used by Default
TMDB_API_KEY get_tmdb_image (movies / TV) (unset — TMDB skipped)
BONES_API_URL anatomy image service http://127.0.0.1:8081

MemiConfig

Passed to create_app. Common fields:

Field Default Purpose
title "memi" Header title.
subtitle "practise…" Header subtitle.
themes 8 built-in themes Available colour themes.
default_theme "light" Initial theme.
sponsor_url None Sponsor link (hidden if None).
about_html None Custom HTML for the about page.
analytics_html None Analytics snippet injected on the page.
favicon_color "#b8860b" Favicon background colour.
wikipedia_lang "en" Wikipedia edition for default images / know more links.
related_sites [] Sibling games to link from the about page.
label_* English strings UI labels (for localization).
html_lang "en" <html lang> — set it for a non-English game; also the details panel's language.
site_url None Canonical origin; taken from the request if unset.
description "" Home page meta description; falls back to about_html.
og_image None Link-preview image (absolute URL).
category_intro "" Visible line on a category page; pattern with {category}/{count}.
label_contents "what's in…" Summary of a category page's collapsed item list.
contents_max 200 Most item names that list will show.
layout "classic" "enriched" adds the details panel and a prominent know more.

For a non-English game, set wikipedia_lang so the default image lookup and the "know more" link resolve against that language's Wikipedia (e.g. "pt"). It can also be set with the MEMI_WIKIPEDIA_LANG environment variable.

All UI strings are label_* fields, so a fully localized game keeps its labels and about_html in its own language while the code stays English.

Instance static files

To serve your own logo or images, point create_app at a static folder; its files take precedence over the engine's:

app = create_app(config, instance_static="/path/to/static")
# served at /static/... , falling back to the engine's static files

Search discoverability

One game is served from many URLs: /, a /<slug> landing page per category, and /about. The category you are playing is a JavaScript variable, so nothing in the markup distinguishes those pages — left alone they are one page repeated, and a crawler indexes one and discards the rest.

The engine gives each of them its own metadata, derived from what the instance already declares:

Page Title Description Canonical
/ {title} — {subtitle} description, else the opening of about_html /
/<slug> {category} — {title} {category} — {description} the category's short slug
/about {label_about} — {title} as the home page /about

The title and description patterns are home_title, category_title and category_description on MemiConfig; a category pattern can also use {count}, the number of items in it. Defaults are punctuation only, so a localized game stays in its own language without touching them.

A category page also renders content a crawler can read, since the game above it is pure JavaScript state: a heading with the category's label, the optional category_intro line, and the category's item names in a <details> that is closed by default — the names are the answers the game asks players to recall, so they belong in the markup without being shown to a player who didn't ask for them. label_contents is that block's summary (localize it like any other label); contents_max caps a long list at 200 names.

Every page also carries a <nav> of real links to each category page. The menu itself is buttons wired to JavaScript, so without it nothing on the site links to a landing page at all.

Two more things fall out of this:

  • /robots.txt opens the site, excludes /api/ (JSON a crawler can do nothing with) and names the sitemap.
  • /sitemap.xml lists /, /about and every category — built from the registry, so a new provider appears without an edit. A category whose last segment is unique answers at both /food and /culture-food; only the short one is listed, and the long one canonicalises to it.

Canonical and og: URLs need the site's real origin. Behind a reverse proxy that sends X-Forwarded-Proto and X-Forwarded-Host (Caddy does by default) the engine works it out per request. Set site_url — or MEMI_SITE_URL — to pin it instead.

Set html_lang per instance. It defaults to "en", and a Portuguese game that tells Google it is English is a game shown to the wrong people.

Deployment

The app is a standard WSGI Flask app. For production, install the server extra and run under gunicorn:

pip install "memi-engine[server]"
gunicorn "yourgame:app"

Two optional data files are read from the working directory at runtime: excluded_items.txt (items to hide) and reported_items.log (written when players report a bad card).

Live examples

Real games built on this engine, all linked from memi.games: memi · memi portugal · memi lisboa · memi slovensko · memi US.

Development

uv sync --extra dev
pytest          # run the test suite
ruff check .    # lint

License

MIT — see LICENSE.

Metadata

Release files for memi-engine 0.10.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for memi-engine 0.10.0
File Size Uploaded
memi_engine-0.10.0.tar.gz 62.4 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for memi-engine 0.10.0
File Interpreter ABI Platform
memi_engine-0.10.0-py3-none-any.whl Python 3 none any Details

Total release size: 130.2 kB

Release files / memi_engine-0.10.0.tar.gz

Download URL memi_engine-0.10.0.tar.gz
Size 62.4 kB
Tags Source
SHA-256 checksum
How to use checksums
58f1ac592c0dfe62533c3c19217033195c057379900e8a2e05a4a449d65404ff
BLAKE2b-256 checksum
How to use checksums
b860655607048d6080708e02817300e5844d99beaaecdfe1af2ab5c2e79ee4cd
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.3

Release files / memi_engine-0.10.0-py3-none-any.whl

Download URL memi_engine-0.10.0-py3-none-any.whl
Size 67.8 kB
Tags Python 3
SHA-256 checksum
How to use checksums
ebb7c498740047cec650b9d01a11d1ac157a0105b4970c92483eaea48f89924d
BLAKE2b-256 checksum
How to use checksums
d2617ba8cc2764562f24496ba03303da0ae2e6f83b0bc1bf3edd1bfac3b77b98
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.3

Release history Release notifications | RSS feed

This release

0.10.0 This release

2 release files

0.9.0

2 release files

0.8.0

2 release files

0.7.0

2 release files

0.6.0

2 release files

0.5.1

2 release files

0.5.0

2 release files

0.4.2

2 release files

0.4.1

2 release files

0.4.0

2 release files

0.3.0

2 release files

0.2.0

2 release files

0.1.1

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page