anki decks: add multiple-choice shapes variant; add real element photos to periodic table

shapes_mc (tools/anki-deck-visual.py) is the same shape images as
"shapes" but multiple choice instead of type-the-name, matching the
plain-text A/B/C/D pattern anki-deck-periodic.py's "category" deck
already uses (no clickable UI, since that needs a desktop-only Anki
add-on and breaks on AnkiDroid/AnkiMobile).

periodic prehs/hs (tools/anki-deck-periodic.py) now show each element's
real sample photo alongside every card, fetched once from Wikipedia's
own MediaWiki pageimages API and cached under tools/periodic_images/ —
the one part of this tooling that needs internet access at generation
time; --no-images restores the old text-only cards. Elements 100
(Fermium) through 118 (Oganesson) are excluded, since none has ever
existed in a photographable quantity — every fetch is otherwise
per-element and non-fatal, with progress printed so failures are visible.

This fetch logic could not be exercised against the real Wikipedia API
from this sandbox (no route to en.wikipedia.org here) — --dry-run-tts
now also substitutes a placeholder image so the pipeline is at least
structurally tested end to end. Real-network behavior needs verifying
on an actual run.

Added tools/*.apkg, tools/media_*/, tools/periodic_images/, and
tools/__pycache__/ to .gitignore — all generated/cached locally, never
meant to be committed.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014DyceEVVeQ33EeS6C1PDv5
This commit is contained in:
Claude
2026-09-10 14:20:24 +00:00
parent e4e32f20e7
commit 67282521ac
4 changed files with 203 additions and 18 deletions
+58 -8
View File
@@ -28,6 +28,12 @@ Decks:
plus 5 quadrilateral types (image->name: square, rectangle,
rhombus, trapezoid, parallelogram — each one's geometry is
genuinely distinct, not just differently labeled)
shapes_mc same shape images as "shapes", but as multiple choice
(A/B/C/D shown as plain text below the image — not a
clickable UI, since that needs a desktop-only Anki add-on
and would break on AnkiDroid/AnkiMobile — same approach as
tools/anki-deck-periodic.py's "category" deck), type the
letter
clocks analog clock faces, all 144 hour/5-min combinations,
type the time as H:MM (the hour hand moves fractionally
with the minutes, e.g. 6:30 sits halfway between 6 and 7 —
@@ -39,6 +45,7 @@ Decks:
Usage (run with the venv from anki-deck-math.py's docstring activated):
python3 anki-deck-visual.py --deck shapes
python3 anki-deck-visual.py --deck shapes_mc
python3 anki-deck-visual.py --deck clocks
python3 anki-deck-visual.py --deck currency
(add --voice en_US-amy-medium etc.; --model-path if a voice isn't found
@@ -56,7 +63,8 @@ from PIL import Image, ImageDraw, ImageFont
import subprocess
parser = argparse.ArgumentParser()
parser.add_argument("--deck", required=True, choices=["shapes", "clocks", "currency"])
parser.add_argument("--deck", required=True,
choices=["shapes", "shapes_mc", "clocks", "currency"])
parser.add_argument("--voice", default="en_US-lessac-medium")
parser.add_argument("--model-path", default=None)
parser.add_argument("--dry-run-tts", action="store_true")
@@ -160,6 +168,8 @@ def build_model(deck_key):
.card { font-family: Arial, sans-serif; font-size: 24px; text-align: center; }
.imgwrap { margin: 10px auto; }
.imgwrap img { max-width: 260px; max-height: 260px; }
.mc-choices { display: inline-block; text-align: left; margin-top: 14px; font-size: 22px; }
.mc-choices div { margin: 4px 0; }
""",
)
@@ -363,6 +373,48 @@ def gen_shapes():
return deck, media_files, len(jobs)
def gen_shapes_mc():
"""Same shape images as gen_shapes(), but multiple choice instead of
type-the-name — A/B/C/D distractors drawn from every other shape name
in the pool (polygons and quadrilaterals share one distractor pool, so
a polygon question can pull "square" as a wrong answer and vice versa)."""
deck_key = "shapes_mc"
model_id, model = build_model(deck_key)
deck = build_deck(deck_key, "Shapes: Polygons & Quadrilaterals (multiple choice)")
media_files = []
jobs = [("polygon", n, POLYGON_NAMES[n]) for n in range(3, 11)]
jobs += [("quad", qname, qname) for qname in QUADRILATERALS]
all_names = [name for _, _, name in jobs]
random.seed(63)
random.shuffle(jobs)
letters = ["A", "B", "C", "D"]
for kind, val, name in jobs:
if kind == "polygon":
png_path = polygon_png(regular_polygon_points(val), f"mc_poly_{val}.png")
tag = f"shape_mc_poly_{val}"
else:
png_path = polygon_png(QUADRILATERALS[val], f"mc_quad_{val}.png")
tag = f"shape_mc_quad_{val}"
media_files.append(png_path)
distractor_pool = [n for n in all_names if n != name]
distractors = random.sample(distractor_pool, 3)
choices = distractors + [name]
random.shuffle(choices)
correct_letter = letters[choices.index(name)]
choice_html = "".join(f"<div>{letter}) {choice}</div>"
for letter, choice in zip(letters, choices))
image_html = (f'<img src="{os.path.basename(png_path)}">'
f'<div class="mc-choices">{choice_html}</div>')
add_note(deck, model, image_html, correct_letter,
"What shape is this?", name, media_files, tag)
return deck, media_files, len(jobs)
def gen_clocks():
deck_key = "clocks"
model_id, model = build_model(deck_key)
@@ -431,17 +483,15 @@ def gen_currency():
return deck, media_files, len(combos)
if args.deck == "shapes":
deck_key = "shapes"
elif args.deck == "clocks":
deck_key = "clocks"
else:
deck_key = "currency"
deck_key = args.deck
MEDIA_DIR = os.path.join(SCRATCH, f"media_{deck_key}_{args.voice}")
os.makedirs(MEDIA_DIR, exist_ok=True)
GENERATORS = {"shapes": gen_shapes, "clocks": gen_clocks, "currency": gen_currency}
GENERATORS = {
"shapes": gen_shapes, "shapes_mc": gen_shapes_mc,
"clocks": gen_clocks, "currency": gen_currency,
}
deck, media_files, count = GENERATORS[args.deck]()
package = genanki.Package(deck)