anki decks: add --skip-ones multiplication/division variant; switch shapes/clocks/currency images from SVG to PNG
--skip-ones drops every fact involving 1 (trivial, not worth drilling), 121 cards instead of 144 for both multiplication and division. SVG images never displayed on AnkiDroid (mobile) despite rendering fine on desktop Anki — a long-documented, still-open AnkiDroid limitation (multiple open ankidroid/Anki-Android GitHub issues), not a bug in the generated markup. Rewrote shape/clock/coin image generation to render PNG via Pillow instead, preserving the same geometry math (regular polygons, clock-hand angles including the fractional hour-hand movement, coin layouts) — card counts and content are unchanged, only the image format. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014DyceEVVeQ33EeS6C1PDv5
This commit is contained in:
@@ -309,11 +309,13 @@ exact usage is documented in `tools/anki-deck-math.py`'s own header
|
|||||||
docstring; the other two scripts point back to it rather than repeating
|
docstring; the other two scripts point back to it rather than repeating
|
||||||
the same instructions three times.
|
the same instructions three times.
|
||||||
|
|
||||||
Shapes, clocks, and coin images are drawn programmatically (SVG) rather
|
Shapes, clocks, and coin images are drawn programmatically (PNG, via
|
||||||
than AI-generated — image generation is a poor fit for content that has
|
Pillow) rather than AI-generated — image generation is a poor fit for
|
||||||
to be exactly correct (an exact clock time, an exact side count), not
|
content that has to be exactly correct (an exact clock time, an exact
|
||||||
just plausible-looking; see `tools/anki-deck-visual.py`'s own docstring
|
side count), not just plausible-looking. PNG rather than SVG specifically
|
||||||
for more on that tradeoff.
|
because AnkiDroid has long-standing, still-open bugs rendering SVG
|
||||||
|
`<img>` tags on mobile; see `tools/anki-deck-visual.py`'s own docstring
|
||||||
|
for more on both tradeoffs.
|
||||||
|
|
||||||
## Layout
|
## Layout
|
||||||
|
|
||||||
|
|||||||
+23
-7
@@ -13,10 +13,13 @@ and services/anki-progress.sh for the actual self-hosted sync backend and
|
|||||||
progress dashboard this content is meant to be studied through.
|
progress dashboard this content is meant to be studied through.
|
||||||
|
|
||||||
Decks:
|
Decks:
|
||||||
multiplication 1-12, all 144 ordered pairs (a x b), shuffled
|
multiplication 1-12, all 144 ordered pairs (a x b), shuffled
|
||||||
(not sequential — see the note near
|
(not sequential — see the note near
|
||||||
random.shuffle(pairs) below for why)
|
random.shuffle(pairs) below for why). Add
|
||||||
division inverse of the multiplication deck (144 facts)
|
--skip-ones to drop every fact involving 1
|
||||||
|
(trivial, 121 facts instead of 144).
|
||||||
|
division inverse of the multiplication deck (144 facts,
|
||||||
|
or 121 with --skip-ones)
|
||||||
addsub --lo L --hi H addition + subtraction fact family for [L, H]
|
addsub --lo L --hi H addition + subtraction fact family for [L, H]
|
||||||
(subtraction facts derived from the addition
|
(subtraction facts derived from the addition
|
||||||
facts, e.g. 7+3=10 also gives 10-7=3 and
|
facts, e.g. 7+3=10 also gives 10-7=3 and
|
||||||
@@ -45,6 +48,7 @@ Setup (one time):
|
|||||||
|
|
||||||
Usage (run with the venv activated):
|
Usage (run with the venv activated):
|
||||||
python3 anki-deck-math.py --deck multiplication
|
python3 anki-deck-math.py --deck multiplication
|
||||||
|
python3 anki-deck-math.py --deck multiplication --skip-ones
|
||||||
python3 anki-deck-math.py --deck division
|
python3 anki-deck-math.py --deck division
|
||||||
python3 anki-deck-math.py --deck addsub --lo 3 --hi 7
|
python3 anki-deck-math.py --deck addsub --lo 3 --hi 7
|
||||||
python3 anki-deck-math.py --deck addsub --lo 3 --hi 13
|
python3 anki-deck-math.py --deck addsub --lo 3 --hi 13
|
||||||
@@ -74,6 +78,10 @@ parser.add_argument("--deck", required=True,
|
|||||||
choices=["multiplication", "division", "addsub", "fractions", "decimals"])
|
choices=["multiplication", "division", "addsub", "fractions", "decimals"])
|
||||||
parser.add_argument("--lo", type=int, default=None, help="addsub only: low end of range")
|
parser.add_argument("--lo", type=int, default=None, help="addsub only: low end of range")
|
||||||
parser.add_argument("--hi", type=int, default=None, help="addsub only: high end of range")
|
parser.add_argument("--hi", type=int, default=None, help="addsub only: high end of range")
|
||||||
|
parser.add_argument("--skip-ones", action="store_true",
|
||||||
|
help="multiplication/division only: drop every fact where either"
|
||||||
|
" number is 1 (1x1..1x12, 2x1..12x1, and their division"
|
||||||
|
" inverses) — those are trivial and not worth drilling.")
|
||||||
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",
|
||||||
@@ -269,11 +277,14 @@ def add_note(deck, model, top, bottom, answer, qtext, atext, media_files, tag):
|
|||||||
|
|
||||||
# ─── Per-deck generators ─────────────────────────────────────────────────────
|
# ─── Per-deck generators ─────────────────────────────────────────────────────
|
||||||
def gen_multiplication():
|
def gen_multiplication():
|
||||||
deck_key = "multiplication"
|
deck_key = "multiplication" + ("_no_ones" if args.skip_ones else "")
|
||||||
model_id, model = build_model(deck_key)
|
model_id, model = build_model(deck_key)
|
||||||
deck = build_deck(deck_key, "Multiplication Facts (1-12)")
|
title = "Multiplication Facts (1-12)" + (", no 1s" if args.skip_ones else "")
|
||||||
|
deck = build_deck(deck_key, title)
|
||||||
media_files = []
|
media_files = []
|
||||||
pairs = [(a, b) for a in range(1, 13) for b in range(1, 13)]
|
pairs = [(a, b) for a in range(1, 13) for b in range(1, 13)]
|
||||||
|
if args.skip_ones:
|
||||||
|
pairs = [(a, b) for a, b in pairs if a != 1 and b != 1]
|
||||||
random.seed(42)
|
random.seed(42)
|
||||||
random.shuffle(pairs)
|
random.shuffle(pairs)
|
||||||
for a, b in pairs:
|
for a, b in pairs:
|
||||||
@@ -285,13 +296,16 @@ def gen_multiplication():
|
|||||||
|
|
||||||
|
|
||||||
def gen_division():
|
def gen_division():
|
||||||
deck_key = "division"
|
deck_key = "division" + ("_no_ones" if args.skip_ones else "")
|
||||||
model_id, model = build_model(deck_key)
|
model_id, model = build_model(deck_key)
|
||||||
deck = build_deck(deck_key, "Division Facts (inverse of 1-12 times tables)")
|
title = "Division Facts (inverse of 1-12 times tables)" + (", no 1s" if args.skip_ones else "")
|
||||||
|
deck = build_deck(deck_key, title)
|
||||||
media_files = []
|
media_files = []
|
||||||
# Same (a, b) pairs as multiplication: product / a = b. This is the
|
# Same (a, b) pairs as multiplication: product / a = b. This is the
|
||||||
# direct inverse of every multiplication card in that deck.
|
# direct inverse of every multiplication card in that deck.
|
||||||
pairs = [(a, b) for a in range(1, 13) for b in range(1, 13)]
|
pairs = [(a, b) for a in range(1, 13) for b in range(1, 13)]
|
||||||
|
if args.skip_ones:
|
||||||
|
pairs = [(a, b) for a, b in pairs if a != 1 and b != 1]
|
||||||
random.seed(43)
|
random.seed(43)
|
||||||
random.shuffle(pairs)
|
random.shuffle(pairs)
|
||||||
for a, b in pairs:
|
for a, b in pairs:
|
||||||
@@ -393,6 +407,8 @@ def gen_decimals():
|
|||||||
# ─── Dispatch ─────────────────────────────────────────────────────────────────
|
# ─── Dispatch ─────────────────────────────────────────────────────────────────
|
||||||
if args.deck == "addsub":
|
if args.deck == "addsub":
|
||||||
deck_key = f"addsub_{args.lo}_{args.hi}"
|
deck_key = f"addsub_{args.lo}_{args.hi}"
|
||||||
|
elif args.deck in ("multiplication", "division") and args.skip_ones:
|
||||||
|
deck_key = f"{args.deck}_no_ones"
|
||||||
else:
|
else:
|
||||||
deck_key = args.deck
|
deck_key = args.deck
|
||||||
|
|
||||||
|
|||||||
+104
-59
@@ -3,9 +3,11 @@
|
|||||||
shapes, clocks, and coin-counting, with Anki's built-in type-the-answer
|
shapes, clocks, and coin-counting, with Anki's built-in type-the-answer
|
||||||
input and Piper (offline, local neural TTS) audio. See
|
input and Piper (offline, local neural TTS) audio. See
|
||||||
tools/anki-deck-math.py's docstring for one-time setup (venv, genanki +
|
tools/anki-deck-math.py's docstring for one-time setup (venv, genanki +
|
||||||
piper-tts, downloading a voice) — same steps apply here.
|
piper-tts, downloading a voice) — same steps apply here, plus one more
|
||||||
|
package this script alone needs: `pip install pillow` (for drawing the
|
||||||
|
images — see the note below on PNG vs SVG for why).
|
||||||
|
|
||||||
All images are drawn programmatically as SVG (regular-polygon geometry,
|
All images are drawn programmatically (regular-polygon geometry,
|
||||||
clock-hand trigonometry, coin layouts) rather than AI-generated — image
|
clock-hand trigonometry, coin layouts) rather than AI-generated — image
|
||||||
generation (local or cloud) is a poor fit for content that has to be
|
generation (local or cloud) is a poor fit for content that has to be
|
||||||
exactly correct (an exact clock time, an exact side count, an exact coin
|
exactly correct (an exact clock time, an exact side count, an exact coin
|
||||||
@@ -13,6 +15,14 @@ total), not just plausible-looking. See this script's own point/angle
|
|||||||
generation functions for how each shape's geometry is computed directly
|
generation functions for how each shape's geometry is computed directly
|
||||||
rather than approximated.
|
rather than approximated.
|
||||||
|
|
||||||
|
Rendered as PNG (via Pillow), not SVG — AnkiDroid has a long-documented
|
||||||
|
history of unreliable SVG rendering (multiple open ankidroid/Anki-Android
|
||||||
|
GitHub issues going back years: some SVGs render, some silently don't,
|
||||||
|
with no clear pattern tied to how the file itself is written). PNG has no
|
||||||
|
such history on any Anki client. Confirmed live: an earlier SVG-based
|
||||||
|
version of this script produced images that displayed fine on desktop
|
||||||
|
Anki but never appeared at all on a mobile client.
|
||||||
|
|
||||||
Decks:
|
Decks:
|
||||||
shapes regular polygons (3-10 sides, image->name and name->sides)
|
shapes regular polygons (3-10 sides, image->name and name->sides)
|
||||||
plus 5 quadrilateral types (image->name: square, rectangle,
|
plus 5 quadrilateral types (image->name: square, rectangle,
|
||||||
@@ -41,6 +51,8 @@ import genanki
|
|||||||
import math
|
import math
|
||||||
import os
|
import os
|
||||||
import random
|
import random
|
||||||
|
|
||||||
|
from PIL import Image, ImageDraw, ImageFont
|
||||||
import subprocess
|
import subprocess
|
||||||
|
|
||||||
parser = argparse.ArgumentParser()
|
parser = argparse.ArgumentParser()
|
||||||
@@ -178,14 +190,45 @@ def add_text_note(deck, model, text, answer, qtext, atext, media_files, tag):
|
|||||||
qtext, atext, media_files, tag)
|
qtext, atext, media_files, tag)
|
||||||
|
|
||||||
|
|
||||||
# ─── SVG generation ───────────────────────────────────────────────────────────
|
# ─── PNG generation (Pillow) ─────────────────────────────────────────────────
|
||||||
def save_svg(svg_body, filename, viewbox="0 0 200 200"):
|
_FONT_CACHE = {}
|
||||||
|
_FONT_PATH_CANDIDATES = [
|
||||||
|
"/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf",
|
||||||
|
"/usr/share/fonts/dejavu/DejaVuSans-Bold.ttf",
|
||||||
|
"/usr/share/fonts/truetype/liberation/LiberationSans-Bold.ttf",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def load_font(size):
|
||||||
|
"""Loads a real bold TTF font if one of the common Debian/Ubuntu paths
|
||||||
|
exists (DejaVu Sans Bold ships with fonts-dejavu-core, a very common
|
||||||
|
baseline package); falls back to Pillow's own scalable default font
|
||||||
|
otherwise, so this never crashes even if no system font is found."""
|
||||||
|
if size in _FONT_CACHE:
|
||||||
|
return _FONT_CACHE[size]
|
||||||
|
for path in _FONT_PATH_CANDIDATES:
|
||||||
|
if os.path.isfile(path):
|
||||||
|
font = ImageFont.truetype(path, size)
|
||||||
|
_FONT_CACHE[size] = font
|
||||||
|
return font
|
||||||
|
font = ImageFont.load_default(size=size)
|
||||||
|
_FONT_CACHE[size] = font
|
||||||
|
return font
|
||||||
|
|
||||||
|
|
||||||
|
def draw_text_centered(draw, xy, text, font, fill="black"):
|
||||||
|
bbox = draw.textbbox((0, 0), text, font=font)
|
||||||
|
w, h = bbox[2] - bbox[0], bbox[3] - bbox[1]
|
||||||
|
x, y = xy
|
||||||
|
draw.text((x - w / 2 - bbox[0], y - h / 2 - bbox[1]), text, font=font, fill=fill)
|
||||||
|
|
||||||
|
|
||||||
|
def save_png(filename, draw_fn, size=(200, 200)):
|
||||||
|
img = Image.new("RGB", size, "white")
|
||||||
|
draw = ImageDraw.Draw(img)
|
||||||
|
draw_fn(draw)
|
||||||
path = os.path.join(MEDIA_DIR, filename)
|
path = os.path.join(MEDIA_DIR, filename)
|
||||||
with open(path, "w") as f:
|
img.save(path)
|
||||||
f.write(
|
|
||||||
f'<svg xmlns="http://www.w3.org/2000/svg" viewBox="{viewbox}" '
|
|
||||||
f'width="200" height="200">{svg_body}</svg>'
|
|
||||||
)
|
|
||||||
return path
|
return path
|
||||||
|
|
||||||
|
|
||||||
@@ -202,9 +245,10 @@ def regular_polygon_points(n_sides, cx=100, cy=100, r=80):
|
|||||||
return points
|
return points
|
||||||
|
|
||||||
|
|
||||||
def polygon_svg(points):
|
def polygon_png(points, filename):
|
||||||
pts_str = " ".join(f"{x},{y}" for x, y in points)
|
def draw_fn(draw):
|
||||||
return f'<polygon points="{pts_str}" fill="#6fa8dc" stroke="#1c4587" stroke-width="4"/>'
|
draw.polygon(points, fill=(111, 168, 220), outline=(28, 69, 135), width=4)
|
||||||
|
return save_png(filename, draw_fn)
|
||||||
|
|
||||||
|
|
||||||
POLYGON_NAMES = {
|
POLYGON_NAMES = {
|
||||||
@@ -221,54 +265,54 @@ QUADRILATERALS = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def clock_svg(hour, minute):
|
def clock_png(hour, minute, filename):
|
||||||
cx, cy, r = 100, 100, 90
|
cx, cy, r = 100, 100, 90
|
||||||
minute_angle = minute * 6 - 90
|
minute_angle = minute * 6 - 90
|
||||||
hour_angle = (hour % 12) * 30 + minute * 0.5 - 90
|
hour_angle = (hour % 12) * 30 + minute * 0.5 - 90
|
||||||
|
|
||||||
def hand(angle_deg, length, width, color):
|
def draw_fn(draw):
|
||||||
rad = math.radians(angle_deg)
|
draw.ellipse([cx - r, cy - r, cx + r, cy + r], fill="white", outline="black", width=3)
|
||||||
x2 = cx + length * math.cos(rad)
|
|
||||||
y2 = cy + length * math.sin(rad)
|
|
||||||
return f'<line x1="{cx}" y1="{cy}" x2="{x2:.1f}" y2="{y2:.1f}" stroke="{color}" stroke-width="{width}" stroke-linecap="round"/>'
|
|
||||||
|
|
||||||
ticks = []
|
font = load_font(15)
|
||||||
numerals = []
|
for h in range(1, 13):
|
||||||
for h in range(1, 13):
|
angle = math.radians(h * 30 - 90)
|
||||||
angle = math.radians(h * 30 - 90)
|
tx1, ty1 = cx + (r - 10) * math.cos(angle), cy + (r - 10) * math.sin(angle)
|
||||||
tx1, ty1 = cx + (r - 10) * math.cos(angle), cy + (r - 10) * math.sin(angle)
|
tx2, ty2 = cx + r * math.cos(angle), cy + r * math.sin(angle)
|
||||||
tx2, ty2 = cx + r * math.cos(angle), cy + r * math.sin(angle)
|
draw.line([(tx1, ty1), (tx2, ty2)], fill="black", width=2)
|
||||||
ticks.append(f'<line x1="{tx1:.1f}" y1="{ty1:.1f}" x2="{tx2:.1f}" y2="{ty2:.1f}" stroke="black" stroke-width="2"/>')
|
nx, ny = cx + (r - 22) * math.cos(angle), cy + (r - 22) * math.sin(angle)
|
||||||
nx, ny = cx + (r - 22) * math.cos(angle), cy + (r - 22) * math.sin(angle)
|
draw_text_centered(draw, (nx, ny), str(h), font)
|
||||||
numerals.append(f'<text x="{nx:.1f}" y="{ny:.1f}" font-size="14" text-anchor="middle" dominant-baseline="middle">{h}</text>')
|
|
||||||
|
|
||||||
body = (
|
def hand(angle_deg, length, width):
|
||||||
f'<circle cx="{cx}" cy="{cy}" r="{r}" fill="white" stroke="black" stroke-width="3"/>'
|
rad = math.radians(angle_deg)
|
||||||
+ "".join(ticks) + "".join(numerals)
|
x2, y2 = cx + length * math.cos(rad), cy + length * math.sin(rad)
|
||||||
+ hand(hour_angle, 45, 6, "black")
|
draw.line([(cx, cy), (x2, y2)], fill="black", width=width)
|
||||||
+ hand(minute_angle, 70, 4, "black")
|
|
||||||
+ f'<circle cx="{cx}" cy="{cy}" r="4" fill="black"/>'
|
hand(hour_angle, 45, 6)
|
||||||
)
|
hand(minute_angle, 70, 4)
|
||||||
return body
|
draw.ellipse([cx - 4, cy - 4, cx + 4, cy + 4], fill="black")
|
||||||
|
|
||||||
|
return save_png(filename, draw_fn)
|
||||||
|
|
||||||
|
|
||||||
COIN_INFO = {5: ("#c0c0c0", "5¢"), 10: ("#d9d9d9", "10¢"), 25: ("#b8b8b8", "25¢")}
|
COIN_INFO = {5: ("#c0c0c0", "5¢"), 10: ("#d9d9d9", "10¢"), 25: ("#b8b8b8", "25¢")}
|
||||||
COIN_NAMES = {5: "nickel", 10: "dime", 25: "quarter"}
|
COIN_NAMES = {5: "nickel", 10: "dime", 25: "quarter"}
|
||||||
|
|
||||||
|
|
||||||
def coins_svg(coin_values):
|
def coins_png(coin_values, filename):
|
||||||
n = len(coin_values)
|
n = len(coin_values)
|
||||||
spacing = 200 // (n + 1)
|
spacing = 200 // (n + 1)
|
||||||
parts = []
|
|
||||||
for i, v in enumerate(coin_values):
|
def draw_fn(draw):
|
||||||
cx = spacing * (i + 1)
|
font = load_font(14)
|
||||||
color, label = COIN_INFO[v]
|
for i, v in enumerate(coin_values):
|
||||||
radius = 30 if v == 25 else (26 if v == 10 else 28)
|
cx = spacing * (i + 1)
|
||||||
parts.append(
|
color, label = COIN_INFO[v]
|
||||||
f'<circle cx="{cx}" cy="100" r="{radius}" fill="{color}" stroke="#444" stroke-width="2"/>'
|
radius = 30 if v == 25 else (26 if v == 10 else 28)
|
||||||
f'<text x="{cx}" y="105" font-size="14" text-anchor="middle">{label}</text>'
|
draw.ellipse([cx - radius, 100 - radius, cx + radius, 100 + radius],
|
||||||
)
|
fill=color, outline=(68, 68, 68), width=2)
|
||||||
return "".join(parts)
|
draw_text_centered(draw, (cx, 100), label, font)
|
||||||
|
|
||||||
|
return save_png(filename, draw_fn)
|
||||||
|
|
||||||
|
|
||||||
def coin_list_words(coin_values):
|
def coin_list_words(coin_values):
|
||||||
@@ -300,9 +344,9 @@ def gen_shapes():
|
|||||||
if kind == "polygon_image":
|
if kind == "polygon_image":
|
||||||
n = val
|
n = val
|
||||||
name = POLYGON_NAMES[n]
|
name = POLYGON_NAMES[n]
|
||||||
svg_path = save_svg(polygon_svg(regular_polygon_points(n)), f"poly_{n}.svg")
|
png_path = polygon_png(regular_polygon_points(n), f"poly_{n}.png")
|
||||||
media_files.append(svg_path)
|
media_files.append(png_path)
|
||||||
add_note(deck, model, f'<img src="poly_{n}.svg">', name,
|
add_note(deck, model, f'<img src="poly_{n}.png">', name,
|
||||||
"What shape is this?", name, media_files, f"shape_img_{n}")
|
"What shape is this?", name, media_files, f"shape_img_{n}")
|
||||||
elif kind == "polygon_sides":
|
elif kind == "polygon_sides":
|
||||||
n = val
|
n = val
|
||||||
@@ -312,9 +356,9 @@ def gen_shapes():
|
|||||||
media_files, f"shape_sides_{n}")
|
media_files, f"shape_sides_{n}")
|
||||||
else:
|
else:
|
||||||
qname = val
|
qname = val
|
||||||
svg_path = save_svg(polygon_svg(QUADRILATERALS[qname]), f"quad_{qname}.svg")
|
png_path = polygon_png(QUADRILATERALS[qname], f"quad_{qname}.png")
|
||||||
media_files.append(svg_path)
|
media_files.append(png_path)
|
||||||
add_note(deck, model, f'<img src="quad_{qname}.svg">', qname,
|
add_note(deck, model, f'<img src="quad_{qname}.png">', qname,
|
||||||
"What shape is this?", qname, media_files, f"shape_quad_{qname}")
|
"What shape is this?", qname, media_files, f"shape_quad_{qname}")
|
||||||
return deck, media_files, len(jobs)
|
return deck, media_files, len(jobs)
|
||||||
|
|
||||||
@@ -336,8 +380,8 @@ def gen_clocks():
|
|||||||
media_files.append(os.path.join(MEDIA_DIR, shared_qfile))
|
media_files.append(os.path.join(MEDIA_DIR, shared_qfile))
|
||||||
|
|
||||||
for hour, minute in times:
|
for hour, minute in times:
|
||||||
svg_path = save_svg(clock_svg(hour, minute), f"clock_{hour}_{minute:02d}.svg")
|
png_path = clock_png(hour, minute, f"clock_{hour}_{minute:02d}.png")
|
||||||
media_files.append(svg_path)
|
media_files.append(png_path)
|
||||||
answer = f"{hour}:{minute:02d}"
|
answer = f"{hour}:{minute:02d}"
|
||||||
afile = f"a_clock_{hour}_{minute:02d}.wav"
|
afile = f"a_clock_{hour}_{minute:02d}.wav"
|
||||||
apath = os.path.join(MEDIA_DIR, afile)
|
apath = os.path.join(MEDIA_DIR, afile)
|
||||||
@@ -345,7 +389,7 @@ def gen_clocks():
|
|||||||
media_files.append(apath)
|
media_files.append(apath)
|
||||||
deck.add_note(genanki.Note(
|
deck.add_note(genanki.Note(
|
||||||
model=model,
|
model=model,
|
||||||
fields=[f'<img src="clock_{hour}_{minute:02d}.svg">', answer,
|
fields=[f'<img src="clock_{hour}_{minute:02d}.png">', answer,
|
||||||
f"[sound:{shared_qfile}]", f"[sound:{afile}]"],
|
f"[sound:{shared_qfile}]", f"[sound:{afile}]"],
|
||||||
))
|
))
|
||||||
return deck, media_files, len(times)
|
return deck, media_files, len(times)
|
||||||
@@ -377,12 +421,13 @@ def gen_currency():
|
|||||||
|
|
||||||
for coin_values in combos:
|
for coin_values in combos:
|
||||||
total = sum(coin_values)
|
total = sum(coin_values)
|
||||||
svg_path = save_svg(coins_svg(list(coin_values)), f"coins_{'_'.join(map(str, coin_values))}.svg")
|
tag = "_".join(map(str, coin_values))
|
||||||
media_files.append(svg_path)
|
png_path = coins_png(list(coin_values), f"coins_{tag}.png")
|
||||||
|
media_files.append(png_path)
|
||||||
qtext = f"How much money is {coin_list_words(list(coin_values))}?"
|
qtext = f"How much money is {coin_list_words(list(coin_values))}?"
|
||||||
atext = f"{num2words(total)} cents"
|
atext = f"{num2words(total)} cents"
|
||||||
add_note(deck, model, f'<img src="coins_{"_".join(map(str, coin_values))}.svg">',
|
add_note(deck, model, f'<img src="coins_{tag}.png">',
|
||||||
str(total), qtext, atext, media_files, f"coins_{'_'.join(map(str, coin_values))}")
|
str(total), qtext, atext, media_files, f"coins_{tag}")
|
||||||
return deck, media_files, len(combos)
|
return deck, media_files, len(combos)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user