← fly.sas.vc · SACAA PPL(A) · operations manual
Every subject follows the same twelve steps. Two of the eight cannot use the audio at all and are marked accordingly. Ticks are saved in this browser, so close the tab whenever you like and pick up where you stopped.
Worth being honest about before you plan around it. The pipeline turns prose into a spoken retrieval drill, and photographs of question pages into marked papers. It cannot help with anything that lives on a chart, a graph or a flight computer, and there will be a residue in every subject that needs sitting down with the book.
| Subject | Audio | Where the method falls short |
|---|---|---|
| Aircraft General | Strong | Schematics, instrument diagrams, magneto and fuel system layouts |
| Principles of Flight | Strong | Anything shaped rather than stated — aerofoil geometry, airflow, lift and drag curves. 110 figures prepared; the plates carry what the audio cannot |
| Human Performance | Strong | Very little. The best fit of the eight |
| Aviation Meteorology | Partial | METAR and TAF decoding, synoptic charts, station circles |
| Navigation | None | Nearly all of it. Chart work and the CRP‑5 are motor skills |
| Flight Performance and Planning | None | Graph interpolation, mass and balance, fuel planning |
| Air Law | Strong | Little, but the source must be current SA material |
| Restricted Radiotelephony | Strong | The practical test. Phraseology drills well; the live exchange does not |
Three tools and one indirection. The tools straighten, clean and label a folder of photographs; the indirection keeps a hand-typed key between the questions and the files, so reprocessing an image never breaks anything that points at it.
| Stage | Tool | Produces |
|---|---|---|
| Straighten | images.html | Two clicks along a line that should be level, corners filled with borrowed paper, then crop |
| Clean | batch.html | Whole folder at once, treatment per image, out as a zip |
| Catalogue | catalogue_images.py | manifest.json, figures.json, review-images.md, upload.sh, and renamed copies |
| Upload | bash upload.sh | Sync to s3://sasvc-aviation/ppl/images/<subject>/, immutable cache headers, no invalidation |
| Link | link_figures.py | Reads the live glossary.json; writes figure-links.json, term-figures.json and review-links.md |
| Merge | link_figures.py --merge | Reuses the reviewed proposals; writes a new glossary with figures and figure_captions added |
| Combine | build_figures.py | Every subject's figures.json into one figures.combined.json, with a diff against the live file |
| Publish | SiteGround | data/glossary.json, data/figures.json, and a bumped ?v= on js/glossary.js |
| Verify | verify_figures.py | Live glossary → figure keys → images on the CDN. Non-zero exit if any link is broken |
The hash is right for the cataloguer — it dedupes identical scans and it changes loudly when a file changes. It is wrong for hand-authoring, because nobody types 0720cac7ee77 into forty records correctly. So figures.json carries a readable key onto the hashed file, and everything else references the key. It is written for you, not by you:
{
"pof-3-8-wing-aspect-ratio-comparison": {
"src": "https://d31aog40626hbr.cloudfront.net/ppl/images/principles-of-flight/…png",
"alt": "Diagram of aerofoil section definitions",
"caption": "A diagram from the manual showing aerofoil section definitions",
"ref": "3-8",
"chapter": "3 Drag", "chapter_no": 3, "figure_no": 8,
"kind": "schematic-diagram",
"role": "plate",
"w": 1800, "h": 1200,
"sha": "f2bc0d96e6a8"
}
}Reprocess that figure and one line changes. Every question and glossary entry keeps working. The sha is kept only so a file can be traced back to its manifest record.
Most figures belong to the glossary, not to a question — Principles of Flight is 103 plates against 7 question figures. A picture next to prose is decoration and does nothing for recall; a picture that carries something the words cannot, or that you have to work, is worth its place. Three roles:
| Role | Renders | Use for |
|---|---|---|
| plate | Beside a glossary entry | The bulk of them. Anything spatial that prose describes badly — the aerofoil section, a frontal cross-section, the thunderstorm life cycle |
| stimulus | Above the stem, before answering | Questions unanswerable without it — the synoptic chart, the SigWx chart, winds and temperatures |
| reveal | With the explanation, after answering | A diagram that would hand over the answer if shown first |
An entry takes an optional figure, or an ordered series where understanding is built rather than shown, and a names list where the figure's value is its labels. Formulas belong here too, as entries in their own right with every symbol defined — the plate then carries the geometry those symbols refer to.
{ "term": "Aerofoil",
"def": "A shape that makes much more lift than drag when air flows past it.",
"series": ["pof-02-venturi",
"pof-02-flat-plate",
"pof-02-aerofoil-development",
"pof-02-aerofoil-definitions"],
"figureRole": "plate",
"names": ["leading edge","trailing edge","chord line","camber"] }catalogue_images.py writes a caption that describes the image, which is
correct for a manifest and useless in a glossary. The glossary caption — plain English,
short, aimed at a reader whose English vocabulary may be small — is written by
link_figures.py and lands in figure_captions on the entry itself.
Keep the manifest alt for the screen-reader text.Two new fields on entries that gained a figure, and nothing else. Existing fields are never altered, no entry is added or removed, and the version is bumped:
{ "id": "chord-line",
"term": "Chord line",
"short": "…", "long": "…", "see_also": [...],
"figures": ["pof-aerofoil-section-definitions",
"pof-aerofoil-line-of-zero-lift"],
"figure_captions": {
"pof-aerofoil-section-definitions": "The side view of a wing. Look at the curve on top."
} }js/glossary.js fetches data/figures.json separately from the glossary and tolerates its absence, so a deployment without it still shows every definition. Plates render after the long text and before See also. Several plates on one entry are numbered as a progression; a single plate is labelled with its book reference. Images carry mix-blend-mode:multiply, which drops the white background of a lightened chart into the cream page so it sits like a knocked-out diagram rather than on a white card. Tapping an image lifts it to full size, because detail in a scanned diagram is often finer than the tray is wide.
Where the concept is movement — relative airflow meeting the leading edge and dividing, the different path over the upper and lower surface, downwash, a thickening boundary layer — a cleaned photograph of a book page carries it poorly at any resolution. The page is a still, and the thing being taught is not. Those are worth redrawing as inline SVG with arrows, exactly as figure-dependent question diagrams already are. Send the plate to the chat and say what needs to move.
{ "chapter": 17, "number": 5,
"figure": "met-17-synoptic-1", "figureRole": "stimulus",
"stem": "The height of the ceiling reported by Port Elizabeth is:",
"options": [...], "answer": "c", "why": "…" }Where a figure's value is its labels, store two versions — the Avex original, and the same image with the labels painted out in clean.html. The drill shows the blank one and asks for the parts; the answer reveals the labelled one.
{ "key": "pof-02-aerofoil-definitions",
"blank": "pof-02-aerofoil-definitions-blank",
"names": ["leading edge","trailing edge","chord line","camber"] }Naming four parts of an aerofoil from a blank drawing is genuine retrieval; looking at a labelled one is not. One extra image per diagram and no new tooling.
A deck item may carry a figure key. The renderer ignores it entirely — nothing changes about the MP3 or the cost. listen.html shows it beside that item in the transcript, so the run stays audio-only and the review afterwards has the picture attached to the fact. This is the cheapest of the three: the deck items already exist and are already tagged by section.
exam.html
must render stimulus and reveal at the right moments, listen.html must show
plates beside transcript items, and both must resolve keys through
data/figures.json. Budget for that before filing forty figures against a
renderer that cannot show them yet.catalogue_images.py carries its own subject list, and --subject is
rejected if it is not on it. It now matches the slugs in data/subjects.json
exactly, including flight-performance-planning and
radiotelephony. If a slug ever changes here, change it there in the same
sitting, or the catalogue step stops with an error naming the list.Kept for reference and for rebuilding on another machine. Nothing here needs doing again.
Static files in public_html: index.html, exam.html, listen.html, manual.html, audio-sampler.html, sw.js, .htaccess, data/, audio/. Adding a subject means writing paper JSON into data/papers/ and listing the ids in data/subjects.json — no code changes.
brew install ffmpeg ocrmypdf awscli jq cd ~/ppl python3 -m venv venv # only if ~/ppl/venv does not exist yet source venv/bin/activate pip install anthropic requests pydub pypdf numpy pillow
Scripts live in ~/ppl with the virtualenv: socratic_audio.py, clean_ocr.py, scan_source.py, audit_deck.py, sections.py, try_voices.py, probe.py, check_sample.py, compare_voice.py, publish_audio.sh, setup_bucket.sh, catalogue_images.py, link_figures.py, verify_figures.py, build_figures.py, prune_deck.py, preflight.py, reconcile_s3.py, add_terms.py, probe_api.py.
export ANTHROPIC_API_KEY=sk-ant-... export ELEVENLABS_API_KEY=... export ASK_VOICE_ID=... # daughter, asks the questions export ANSWER_VOICE_ID=... # John, gives the answers export FLY_BUCKET=sasvc-aviation export FLY_PREFIX=ppl/audio export FLY_REGION=us-east-1 export FLY_DISTRIBUTION=E2ALRGRER9YSF0 export FLY_AUDIO_BASE=https://d31aog40626hbr.cloudfront.net/ppl/audio
Images go to the same bucket under ppl/images/<subject>/ and are served from https://d31aog40626hbr.cloudfront.net/ppl/images/. That destination is fixed inside catalogue_images.py rather than set here, because it never varies — the generated upload.sh and the URLs in figures.json both come from it.
In ~/.zshrc. The ElevenLabs key needs Write on both Voices and Text to Speech. Model eleven_flash_v2_5, profile v3 — stability 0.50, similarity 0.85, style 0.10, speed 0.95 — found by scoring samples in audio-sampler.html, not by reasoning.
<slug>[_drill][-pNN].mp3 on the CDN, transcript of the same stem beside it, <slug>[_drill]-index.json for the part list. MP3s to S3; transcripts to SiteGround, because the page fetches those with JavaScript and that needs same-origin.