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:
@@ -1,2 +1,10 @@
|
|||||||
# Vanilla Tweaks ZIP files placed for automatic install — never commit these
|
# Vanilla Tweaks ZIP files placed for automatic install — never commit these
|
||||||
extras/datapacks/*.zip
|
extras/datapacks/*.zip
|
||||||
|
|
||||||
|
# tools/anki-deck-*.py output and caches — generated locally, regenerable,
|
||||||
|
# never meant to be committed (periodic_images/ especially: fetched
|
||||||
|
# element photos, not source)
|
||||||
|
tools/*.apkg
|
||||||
|
tools/media_*/
|
||||||
|
tools/periodic_images/
|
||||||
|
tools/__pycache__/
|
||||||
|
|||||||
@@ -300,8 +300,12 @@ account has no content — `tools/anki-deck-math.py`,
|
|||||||
`tools/anki-deck-periodic.py`, and `tools/anki-deck-visual.py` generate
|
`tools/anki-deck-periodic.py`, and `tools/anki-deck-visual.py` generate
|
||||||
ready-to-import `.apkg` decks (multiplication/division/addition/
|
ready-to-import `.apkg` decks (multiplication/division/addition/
|
||||||
subtraction/fractions/decimals, the periodic table, and shapes/clocks/
|
subtraction/fractions/decimals, the periodic table, and shapes/clocks/
|
||||||
coin-counting) with Anki's built-in type-the-answer input and offline
|
coin-counting) with offline neural TTS audio (Piper) on every card. Most
|
||||||
neural TTS audio (Piper) on every card. All three are standalone Python
|
cards use Anki's built-in type-the-answer input; a few (periodic table
|
||||||
|
categories, `--deck shapes_mc`) are multiple choice instead, shown as
|
||||||
|
plain-text A/B/C/D options — type the letter rather than tapping, since a
|
||||||
|
clickable UI needs a desktop-only Anki add-on and would break on
|
||||||
|
AnkiDroid/AnkiMobile. All three are standalone Python
|
||||||
scripts, unrelated to the `services/*.sh` installer framework — run them
|
scripts, unrelated to the `services/*.sh` installer framework — run them
|
||||||
on any machine with Python, not necessarily the server itself. Full setup
|
on any machine with Python, not necessarily the server itself. Full setup
|
||||||
(a venv, `genanki` + `piper-tts`, downloading a voice) and every deck's
|
(a venv, `genanki` + `piper-tts`, downloading a voice) and every deck's
|
||||||
@@ -317,6 +321,15 @@ because AnkiDroid has long-standing, still-open bugs rendering SVG
|
|||||||
`<img>` tags on mobile; see `tools/anki-deck-visual.py`'s own docstring
|
`<img>` tags on mobile; see `tools/anki-deck-visual.py`'s own docstring
|
||||||
for more on both tradeoffs.
|
for more on both tradeoffs.
|
||||||
|
|
||||||
|
The periodic table `prehs`/`hs` decks also show each element's real
|
||||||
|
sample photo (fetched once from Wikipedia's own MediaWiki API and cached
|
||||||
|
under `tools/periodic_images/`) alongside every card — the one part of
|
||||||
|
this tooling that needs internet access at generation time; pass
|
||||||
|
`--no-images` for the old text-only cards. Elements 100 (Fermium) through
|
||||||
|
118 (Oganesson) are skipped, since none has ever existed in a
|
||||||
|
photographable quantity. See `tools/anki-deck-periodic.py`'s own
|
||||||
|
docstring for the details and its caveats.
|
||||||
|
|
||||||
## Layout
|
## Layout
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|||||||
+122
-8
@@ -16,33 +16,130 @@ Decks:
|
|||||||
superheavy elements whose category is still officially
|
superheavy elements whose category is still officially
|
||||||
unconfirmed)
|
unconfirmed)
|
||||||
|
|
||||||
|
Element photos on prehs/hs: every card (symbol->name, name->symbol,
|
||||||
|
number->symbol alike) also shows a real photo of the element, fetched
|
||||||
|
once from Wikipedia's own MediaWiki API (the same "pageimage" shown in
|
||||||
|
that element's infobox — the well-documented, standard
|
||||||
|
`action=query&prop=pageimages` endpoint on en.wikipedia.org, not a
|
||||||
|
guessed URL) and cached locally under tools/periodic_images/ so reruns
|
||||||
|
don't re-download. This is the one part of this repo's Anki tooling that
|
||||||
|
needs internet access at generation time — every other deck (math,
|
||||||
|
shapes/clocks/currency, TTS audio) is fully offline. Pass --no-images to
|
||||||
|
skip this and get the old text-only prehs/hs cards back.
|
||||||
|
|
||||||
|
Elements 100 (Fermium) through 118 (Oganesson) are hard-excluded from
|
||||||
|
this — every atom of these ever made has been produced (or claimed) one
|
||||||
|
at a time in a particle accelerator and never existed in macroscopic,
|
||||||
|
visible quantity, so no real sample photo exists to fetch; anything
|
||||||
|
Wikipedia's pageimage API returned for them would be a diagram or a
|
||||||
|
scientist's portrait, not the element. A couple of element names collide
|
||||||
|
with a more famous Wikipedia topic under the same plain title (Mercury
|
||||||
|
the planet, for one) — TITLE_OVERRIDES below is the fix-up list; if a
|
||||||
|
generated card shows an obviously wrong photo for some element, that's
|
||||||
|
almost certainly another one of these collisions — add it there.
|
||||||
|
|
||||||
|
Caveat: this fetch logic was written and dry-run tested without live
|
||||||
|
access to Wikipedia (sandboxed here with no route to en.wikipedia.org),
|
||||||
|
so it's exercised structurally (see --dry-run-tts) but not against the
|
||||||
|
real API response shape or real image content. Skips are per-element and
|
||||||
|
non-fatal — one bad/missing photo won't abort the rest of the deck — and
|
||||||
|
every fetch attempt prints what it did, so check that output the first
|
||||||
|
time you actually run this deck for real.
|
||||||
|
|
||||||
Element data: Bowserinator/Periodic-Table-JSON (a widely used, actively
|
Element data: Bowserinator/Periodic-Table-JSON (a widely used, actively
|
||||||
maintained public dataset), fetched and spot-checked against known facts
|
maintained public dataset), fetched and spot-checked against known facts
|
||||||
before being embedded below — not typed from memory.
|
before being embedded below — not typed from memory.
|
||||||
|
|
||||||
Usage (run with the venv from anki-deck-math.py's docstring activated):
|
Usage (run with the venv from anki-deck-math.py's docstring activated;
|
||||||
|
also needs `pip install pillow` for resizing fetched photos):
|
||||||
python3 anki-deck-periodic.py --deck prehs
|
python3 anki-deck-periodic.py --deck prehs
|
||||||
python3 anki-deck-periodic.py --deck hs
|
python3 anki-deck-periodic.py --deck hs
|
||||||
|
python3 anki-deck-periodic.py --deck hs --no-images
|
||||||
python3 anki-deck-periodic.py --deck category
|
python3 anki-deck-periodic.py --deck category
|
||||||
(add --voice en_US-amy-medium etc.; --model-path if a voice isn't found
|
(add --voice en_US-amy-medium etc.; --model-path if a voice isn't found
|
||||||
automatically; --dry-run-tts to test the deck-building logic without any
|
automatically; --dry-run-tts to test the deck-building logic without any
|
||||||
voice model at all, using silent placeholder audio)
|
voice model OR network access at all, using silent placeholder audio and
|
||||||
|
a placeholder image)
|
||||||
"""
|
"""
|
||||||
import argparse
|
import argparse
|
||||||
import hashlib
|
import hashlib
|
||||||
|
import io
|
||||||
import genanki
|
import genanki
|
||||||
|
import json
|
||||||
import os
|
import os
|
||||||
import random
|
import random
|
||||||
import subprocess
|
import subprocess
|
||||||
|
import urllib.error
|
||||||
|
import urllib.parse
|
||||||
|
import urllib.request
|
||||||
|
|
||||||
|
from PIL import Image
|
||||||
|
|
||||||
parser = argparse.ArgumentParser()
|
parser = argparse.ArgumentParser()
|
||||||
parser.add_argument("--deck", required=True, choices=["prehs", "hs", "category"])
|
parser.add_argument("--deck", required=True, choices=["prehs", "hs", "category"])
|
||||||
parser.add_argument("--voice", default="en_US-lessac-medium")
|
parser.add_argument("--voice", default="en_US-lessac-medium")
|
||||||
parser.add_argument("--model-path", default=None)
|
parser.add_argument("--model-path", default=None)
|
||||||
parser.add_argument("--dry-run-tts", action="store_true")
|
parser.add_argument("--dry-run-tts", action="store_true")
|
||||||
|
parser.add_argument("--no-images", action="store_true",
|
||||||
|
help="prehs/hs only: skip fetching element photos, keep the old text-only cards")
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
SCRATCH = os.path.dirname(os.path.abspath(__file__))
|
SCRATCH = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
IMAGE_CACHE = os.path.join(SCRATCH, "periodic_images")
|
||||||
|
os.makedirs(IMAGE_CACHE, exist_ok=True)
|
||||||
|
|
||||||
|
# Elements with no macroscopic sample ever produced — see the docstring.
|
||||||
|
NO_PHOTO_NUMBERS = set(range(100, 119))
|
||||||
|
|
||||||
|
# Element names whose plain Wikipedia article title is a different, more
|
||||||
|
# famous topic — see the docstring. Add to this if a generated card shows
|
||||||
|
# an obviously wrong photo for some element.
|
||||||
|
TITLE_OVERRIDES = {"Hg": "Mercury (element)"}
|
||||||
|
|
||||||
|
|
||||||
|
def fetch_element_image(symbol, name):
|
||||||
|
"""Downloads (once, cached under periodic_images/) the real sample
|
||||||
|
photo Wikipedia itself shows for this element — its page's
|
||||||
|
'pageimage', the same image used in the infobox — via the standard
|
||||||
|
MediaWiki query API. Returns the cached local path, or None if no
|
||||||
|
image exists on the page (normal for some elements, not a bug)."""
|
||||||
|
cache_path = os.path.join(IMAGE_CACHE, f"{symbol}.jpg")
|
||||||
|
if os.path.isfile(cache_path):
|
||||||
|
return cache_path
|
||||||
|
|
||||||
|
if args.dry_run_tts:
|
||||||
|
# No real network call in dry-run mode — a flat placeholder lets the
|
||||||
|
# rest of the pipeline (HTML wiring, media_files list, .apkg
|
||||||
|
# packaging) still be exercised end-to-end without it.
|
||||||
|
Image.new("RGB", (100, 100), (190, 190, 190)).save(cache_path, "JPEG")
|
||||||
|
return cache_path
|
||||||
|
|
||||||
|
title = TITLE_OVERRIDES.get(symbol, name)
|
||||||
|
api_url = ("https://en.wikipedia.org/w/api.php?action=query&format=json"
|
||||||
|
"&prop=pageimages&piprop=original&redirects=1&titles="
|
||||||
|
+ urllib.parse.quote(title))
|
||||||
|
headers = {"User-Agent": "anki-deck-periodic.py (personal Anki deck generator)"}
|
||||||
|
try:
|
||||||
|
req = urllib.request.Request(api_url, headers=headers)
|
||||||
|
with urllib.request.urlopen(req, timeout=15) as resp:
|
||||||
|
data = json.load(resp)
|
||||||
|
pages = data.get("query", {}).get("pages", {})
|
||||||
|
page = next(iter(pages.values()), {})
|
||||||
|
image_url = page.get("original", {}).get("source")
|
||||||
|
if not image_url:
|
||||||
|
print(f" (no photo found for {name})")
|
||||||
|
return None
|
||||||
|
img_req = urllib.request.Request(image_url, headers=headers)
|
||||||
|
with urllib.request.urlopen(img_req, timeout=15) as resp:
|
||||||
|
raw = resp.read()
|
||||||
|
img = Image.open(io.BytesIO(raw)).convert("RGB")
|
||||||
|
img.thumbnail((300, 300))
|
||||||
|
img.save(cache_path, "JPEG", quality=85)
|
||||||
|
print(f" fetched photo for {name} ({title})")
|
||||||
|
return cache_path
|
||||||
|
except (urllib.error.URLError, urllib.error.HTTPError, OSError, ValueError) as e:
|
||||||
|
print(f" (photo fetch failed for {name}: {e})")
|
||||||
|
return None
|
||||||
|
|
||||||
_CANDIDATES = [
|
_CANDIDATES = [
|
||||||
args.model_path,
|
args.model_path,
|
||||||
@@ -257,6 +354,7 @@ def build_model(deck_key):
|
|||||||
css="""
|
css="""
|
||||||
.card { font-family: Arial, sans-serif; font-size: 26px; text-align: center; }
|
.card { font-family: Arial, sans-serif; font-size: 26px; text-align: center; }
|
||||||
.prompt { font-size: 40px; margin: 20px auto; white-space: pre-line; }
|
.prompt { font-size: 40px; margin: 20px auto; white-space: pre-line; }
|
||||||
|
.elem-img { max-width: 220px; max-height: 220px; display: block; margin: 0 auto 10px; }
|
||||||
""",
|
""",
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -267,6 +365,20 @@ def build_deck(deck_key, deck_title):
|
|||||||
return genanki.Deck(deck_id, deck_title)
|
return genanki.Deck(deck_id, deck_title)
|
||||||
|
|
||||||
|
|
||||||
|
def element_image_html(number, symbol, name, media_files):
|
||||||
|
"""Returns an <img> tag for this element's cached/fetched photo, or ""
|
||||||
|
if --no-images was passed, the element is in NO_PHOTO_NUMBERS, or no
|
||||||
|
photo could be found/downloaded for it."""
|
||||||
|
if args.no_images or number in NO_PHOTO_NUMBERS:
|
||||||
|
return ""
|
||||||
|
local_path = fetch_element_image(symbol, name)
|
||||||
|
if local_path is None:
|
||||||
|
return ""
|
||||||
|
if local_path not in media_files:
|
||||||
|
media_files.append(local_path)
|
||||||
|
return f'<img class="elem-img" src="{os.path.basename(local_path)}">'
|
||||||
|
|
||||||
|
|
||||||
def add_note(deck, model, prompt, answer, qtext, atext, media_files, tag):
|
def add_note(deck, model, prompt, answer, qtext, atext, media_files, tag):
|
||||||
qfile = f"q_{tag}.wav"
|
qfile = f"q_{tag}.wav"
|
||||||
afile = f"a_{tag}.wav"
|
afile = f"a_{tag}.wav"
|
||||||
@@ -294,12 +406,13 @@ def gen_prehs():
|
|||||||
random.seed(50)
|
random.seed(50)
|
||||||
random.shuffle(cards)
|
random.shuffle(cards)
|
||||||
for kind, number, symbol, name in cards:
|
for kind, number, symbol, name in cards:
|
||||||
|
img = element_image_html(number, symbol, name, media_files)
|
||||||
if kind == "symbol_to_name":
|
if kind == "symbol_to_name":
|
||||||
add_note(deck, model, symbol, name,
|
add_note(deck, model, img + symbol, name,
|
||||||
f"What element has the symbol {symbol}?", name,
|
f"What element has the symbol {symbol}?", name,
|
||||||
media_files, f"prehs_s2n_{number}")
|
media_files, f"prehs_s2n_{number}")
|
||||||
else:
|
else:
|
||||||
add_note(deck, model, name, symbol,
|
add_note(deck, model, img + name, symbol,
|
||||||
f"What is the symbol for {name}?", symbol,
|
f"What is the symbol for {name}?", symbol,
|
||||||
media_files, f"prehs_n2s_{number}")
|
media_files, f"prehs_n2s_{number}")
|
||||||
return deck, media_files, len(cards)
|
return deck, media_files, len(cards)
|
||||||
@@ -318,16 +431,17 @@ def gen_hs():
|
|||||||
random.seed(51)
|
random.seed(51)
|
||||||
random.shuffle(cards)
|
random.shuffle(cards)
|
||||||
for kind, number, symbol, name in cards:
|
for kind, number, symbol, name in cards:
|
||||||
|
img = element_image_html(number, symbol, name, media_files)
|
||||||
if kind == "symbol_to_name":
|
if kind == "symbol_to_name":
|
||||||
add_note(deck, model, symbol, name,
|
add_note(deck, model, img + symbol, name,
|
||||||
f"What element has the symbol {symbol}?", name,
|
f"What element has the symbol {symbol}?", name,
|
||||||
media_files, f"hs_s2n_{number}")
|
media_files, f"hs_s2n_{number}")
|
||||||
elif kind == "name_to_symbol":
|
elif kind == "name_to_symbol":
|
||||||
add_note(deck, model, name, symbol,
|
add_note(deck, model, img + name, symbol,
|
||||||
f"What is the symbol for {name}?", symbol,
|
f"What is the symbol for {name}?", symbol,
|
||||||
media_files, f"hs_n2s_{number}")
|
media_files, f"hs_n2s_{number}")
|
||||||
else:
|
else:
|
||||||
add_note(deck, model, f"Element #{number}", symbol,
|
add_note(deck, model, img + f"Element #{number}", symbol,
|
||||||
f"What is the symbol for element number {num2words(number)}?", symbol,
|
f"What is the symbol for element number {num2words(number)}?", symbol,
|
||||||
media_files, f"hs_num2s_{number}")
|
media_files, f"hs_num2s_{number}")
|
||||||
return deck, media_files, len(cards)
|
return deck, media_files, len(cards)
|
||||||
@@ -382,4 +496,4 @@ out_path = os.path.join(SCRATCH, f"{deck_key}_{args.voice}.apkg")
|
|||||||
package.write_to_file(out_path)
|
package.write_to_file(out_path)
|
||||||
|
|
||||||
size_mb = os.path.getsize(out_path) / (1024 * 1024)
|
size_mb = os.path.getsize(out_path) / (1024 * 1024)
|
||||||
print(f"\nDone: {out_path} ({size_mb:.1f} MB, {count} cards, {len(media_files)} audio clips)")
|
print(f"\nDone: {out_path} ({size_mb:.1f} MB, {count} cards, {len(media_files)} media files)")
|
||||||
|
|||||||
@@ -28,6 +28,12 @@ Decks:
|
|||||||
plus 5 quadrilateral types (image->name: square, rectangle,
|
plus 5 quadrilateral types (image->name: square, rectangle,
|
||||||
rhombus, trapezoid, parallelogram — each one's geometry is
|
rhombus, trapezoid, parallelogram — each one's geometry is
|
||||||
genuinely distinct, not just differently labeled)
|
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,
|
clocks analog clock faces, all 144 hour/5-min combinations,
|
||||||
type the time as H:MM (the hour hand moves fractionally
|
type the time as H:MM (the hour hand moves fractionally
|
||||||
with the minutes, e.g. 6:30 sits halfway between 6 and 7 —
|
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):
|
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
|
||||||
|
python3 anki-deck-visual.py --deck shapes_mc
|
||||||
python3 anki-deck-visual.py --deck clocks
|
python3 anki-deck-visual.py --deck clocks
|
||||||
python3 anki-deck-visual.py --deck currency
|
python3 anki-deck-visual.py --deck currency
|
||||||
(add --voice en_US-amy-medium etc.; --model-path if a voice isn't found
|
(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
|
import subprocess
|
||||||
|
|
||||||
parser = argparse.ArgumentParser()
|
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("--voice", default="en_US-lessac-medium")
|
||||||
parser.add_argument("--model-path", default=None)
|
parser.add_argument("--model-path", default=None)
|
||||||
parser.add_argument("--dry-run-tts", action="store_true")
|
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; }
|
.card { font-family: Arial, sans-serif; font-size: 24px; text-align: center; }
|
||||||
.imgwrap { margin: 10px auto; }
|
.imgwrap { margin: 10px auto; }
|
||||||
.imgwrap img { max-width: 260px; max-height: 260px; }
|
.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)
|
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():
|
def gen_clocks():
|
||||||
deck_key = "clocks"
|
deck_key = "clocks"
|
||||||
model_id, model = build_model(deck_key)
|
model_id, model = build_model(deck_key)
|
||||||
@@ -431,17 +483,15 @@ def gen_currency():
|
|||||||
return deck, media_files, len(combos)
|
return deck, media_files, len(combos)
|
||||||
|
|
||||||
|
|
||||||
if args.deck == "shapes":
|
deck_key = args.deck
|
||||||
deck_key = "shapes"
|
|
||||||
elif args.deck == "clocks":
|
|
||||||
deck_key = "clocks"
|
|
||||||
else:
|
|
||||||
deck_key = "currency"
|
|
||||||
|
|
||||||
MEDIA_DIR = os.path.join(SCRATCH, f"media_{deck_key}_{args.voice}")
|
MEDIA_DIR = os.path.join(SCRATCH, f"media_{deck_key}_{args.voice}")
|
||||||
os.makedirs(MEDIA_DIR, exist_ok=True)
|
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]()
|
deck, media_files, count = GENERATORS[args.deck]()
|
||||||
|
|
||||||
package = genanki.Package(deck)
|
package = genanki.Package(deck)
|
||||||
|
|||||||
Reference in New Issue
Block a user