# AWARE (Aviation Weighted Active Recall Engine) — Developer Documentation ## Overview A cross-platform, text-based flashcard quiz application written in Python 3. No external dependencies beyond the standard library. Optional text-to-speech uses OS-native tools (no pip packages required). Core features: - 5-bucket weighted spaced repetition - Cross-platform TTS (Linux, macOS, Windows) - Multi-deck support with per-deck progress tracking - Category filtering within decks - Editable pronunciation glossary for TTS ## File Layout ``` aware/ ← project root (SCRIPT_DIR) ├── aware.py ← main application ├── tts_glossary.json ← TTS pronunciation glossary ├── decks/ ← deck storage │ ├── g700_oral_study.json ← 299 questions, 15 categories │ ├── g700_abbreviations.json ← 131 questions, 6 categories │ └── your_deck.json ← any .json file here is auto-discovered │ ~/.aware/ ← user data (created automatically) ├── progress/ ← per-deck progress files │ ├── g700_oral_study_a1b2c3d4e5f6.json │ └── your_deck_f6e5d4c3b2a1.json └── config.json ← optional TTS override config ``` ### Path Resolution | Item | Location | Notes | |------|----------|-------| | `SCRIPT_DIR` | Directory containing `aware.py` | All relative paths anchor here | | `DECKS_DIR` | `SCRIPT_DIR/decks/` | Auto-scanned for `.json` files on startup | | `GLOSSARY_FILE` | `SCRIPT_DIR/tts_glossary.json` | Optional; TTS works without it | | `PROGRESS_DIR` | `~/.aware/progress/` | Created automatically on first save | | `CONFIG_FILE` | `~/.aware/config.json` | Optional; overrides TTS backend | Progress files are named `{deck_stem}_{md5_hash}.json` where the hash is derived from the deck's absolute file path. This means moving a deck file to a different directory creates a new progress file (old progress is orphaned but not lost). ## Deck File Format Decks are JSON files placed in the `decks/` directory. The app supports two formats: categorized (with topic groupings) and flat (just a list). ### Minimal Flat Format ```json { "title": "My Study Deck", "questions": [ { "question": "What is the answer to this?", "answer": "This is the answer." }, { "question": "Another question?", "answer": "Another answer." } ] } ``` Required fields per question: `question`, `answer`. Everything else is optional and auto-populated at load time. ### Categorized Format (single category example) ```json { "title": "My Study Deck", "categories": [ { "name": "My Topic", "questions": [ { "question": "What is the answer to this?", "answer": "This is the answer." }, { "question": "Another question?", "answer": "Another answer." } ] } ] } ``` To add more categories, add more objects to the `categories` array. Empty categories are allowed (the app ignores them until populated). ### Optional Question Fields | Field | Type | Default | Purpose | |-------|------|---------|---------| | `id` | int | Auto-assigned (1, 2, 3...) | Tracks progress per question | | `ref` | string | `""` | Source reference, displayed after the answer | | `category` | string | Parent category name | Set automatically from the category structure | | `cas` | string or array | none | CAS message(s) displayed above the question | | `detail` | string | none | Extended explanation displayed after the answer | ### CAS and Detail Example ```json { "question": "What actions would you take?", "answer": "REFER TO CHECKLIST.", "ref": "AFM, APU Fire (U)", "cas": [ "\u001b[1;31;40mAPU FIRE (U)\u001b[0m", "\u001b[1;33;40m>APU Fail\u001b[0m" ], "detail": "The APU will automatically shut down when fire is detected. You must still discharge the fire bottle into the APU enclosure by selecting FIRE EXT on the Overhead Panel." } ``` `cas` accepts a single string or an array of strings. Each message is displayed on its own line above the question, and ANSI color codes are supported for realistic CAS message presentation. The TTS engine strips ANSI before speaking but includes the CAS text as context. `detail` is displayed after the answer and ref, providing space for deeper explanation, mnemonics, or study notes without cluttering the core answer you're trying to memorize. If you supply your own `id` values, they must be unique within the deck. If omitted, IDs are assigned sequentially at load time. Note: auto-assigned IDs are positional, so inserting questions in the middle of a deck will shift IDs and orphan existing progress. If you plan to actively edit a deck while studying it, assign explicit IDs. ## Progress Files Progress is stored as JSON in `~/.aware/progress/`. Each deck gets its own file. The structure: ```json { "buckets": { "1": 3, "42": 5, "103": 1 }, "stats": { "total_sessions": 12, "total_correct": 85, "total_wrong": 30, "total_partial": 45 } } ``` `buckets` maps question ID (as string) to bucket number (1-5). Questions not present in the map default to bucket 1. ### Resetting Progress Two ways: 1. Menu option 7 inside the app (writes zeroed-out progress) 2. Delete the progress file directly: ```bash rm ~/.aware/progress/deckname_*.json # single deck rm -rf ~/.aware/progress/ # all decks ``` On next launch, the app finds no file and starts from zero. ## Weighted Repetition System Questions are assigned to 5 buckets. Higher buckets mean better retention. Weighted random selection ensures weak material appears more frequently. ### Bucket Weights | Bucket | Label | Selection Weight | Meaning | |--------|-------|-----------------|---------| | 1 | New / No Clue | 8x | Haven't seen it or can't answer at all | | 2 | Some | 5x | Getting some of the answer | | 3 | Half | 3x | Getting about half right | | 4 | Most | 2x | Getting most but not all | | 5 | Nailed It | 1x | Consistently correct | A bucket 1 question is 8x more likely to be selected than a bucket 5 question. ### Grading After revealing an answer, the user grades their recall: | Input | Alias | Action | Description | |-------|-------|--------|-------------| | `n` or Enter | `-` | Set to bucket 1 | Didn't get the answer at all | | `1` | | Set to bucket 2 | Got some of the answer | | `2` | | Set to bucket 3 | Got about half right | | `3` | | Set to bucket 4 | Got most of the answer | | `y` | `+` | Promote by 1 (max 5) | Nailed it | Grades `n` through `3` set the bucket absolutely — a question in bucket 5 graded as `1` drops to bucket 2. Grade `y` is the only relative operation: it promotes by one. This means reaching bucket 5 requires nailing the question while already in bucket 4, enforcing the "getting it right regularly" pattern from physical flashcard study. ### Other Controls During Quiz | Input | Action | |-------|--------| | `v` | Toggle text-to-speech on/off | | `q` | Quit session (progress is saved) | ## Text-to-Speech ### Platform Autodetection | Platform | Backend | Command | |----------|---------|---------| | Linux | espeak-ng | `espeak-ng -v en-us -s 160 "text"` | | macOS | say | `say -v Alex -r 180 "text"` | | Windows | SAPI via PowerShell | `powershell -Command "...SpeechSynthesizer..."` | Detection is by `sys.platform`. The TTS engine runs as a background subprocess — it starts when a question is displayed, and is killed when the user presses Enter to reveal the answer. If the TTS tool is not installed, the app runs fine without voice. The `v` toggle will show an install hint for the current platform. ### Custom TTS Override Create `~/.aware/config.json`: ```json { "tts_command": "piper", "tts_args": ["--model", "en_US-lessac-medium", "--output-raw", "{text}"] } ``` `{text}` is replaced with the spoken text at runtime. The custom command must accept text as an argument and play audio. If the command needs a pipeline (e.g., piper piping to aplay), wrap it in a shell script and point `tts_command` at the script. ### Pronunciation Glossary `tts_glossary.json` controls how abbreviations are spoken. It sits next to `aware.py` and is loaded once at startup. ```json { "abbreviations": { "APU": "A-P-U", "DU": "display unit", "FADEC": "FADEC", "KCAS": "K-CAS" }, "symbols": { ">=": "greater than or equal to", "°": " degrees ", "\n": ". " } } ``` **Abbreviations** are matched using word-boundary regex (`\b`), so `DU` won't corrupt words like `DURING` or `PROCEDURE`. Matches are sorted longest-first at compile time, so `KCAS` is caught before `CAS`. **Symbols** are replaced with simple string substitution, sorted by length (longest first) so `>=` is matched before `>`. If the glossary file is missing, TTS still works — text passes through without any abbreviation expansion. ## Menu Structure ``` Startup └── Deck Selection (if multiple decks exist; auto-selects if only one) └── Deck Menu ├── 1) Quick 10 ├── 2) Session 25 ├── 3) Full 50 ├── 4) Custom count ├── 5) By category (only shown if deck has multiple categories) ├── 6) Review missed only (bucket 1 questions) ├── v) Toggle voice ├── 7) Reset deck progress ├── d) Switch deck └── q) Quit ``` ## Key Functions | Function | Purpose | |----------|---------| | `main()` | Entry point. Creates decks dir, migrates legacy files, runs deck selection loop | | `deck_menu(deck_path, tts)` | Main menu for a loaded deck. Returns `"quit"` or `"switch"` | | `run_quiz(questions, deck_path, progress, count, title, tts)` | Core quiz loop | | `discover_decks()` | Scans `decks/` for valid `.json` files, returns metadata list | | `load_deck(path)` | Parses a deck file, returns `(questions, categories, title)` | | `pick_weighted(questions, progress)` | Selects a question using bucket-weighted random | | `grade_question(progress, qid, grade)` | Applies a grade, returns new bucket number | | `load_progress(deck_path)` | Loads progress JSON or returns fresh defaults | | `save_progress(deck_path, progress)` | Writes progress JSON to `PROGRESS_DIR` | | `select_categories(categories)` | Interactive category picker, returns list or None | | `review_missed(...)` | Filters to bucket 1 questions and runs a quiz on them | ### TTS Class Methods | Method | Purpose | |--------|---------| | `TTS()` | Init: autodetect platform, load glossary, check availability | | `.toggle()` | Flip on/off. Returns new state. Returns `False` if unavailable | | `.speak(text)` | Clean text and speak in background. Kills any prior speech | | `.stop()` | Kill running speech subprocess | | `.install_hint()` | Returns platform-specific install instructions string | ## ANSI Color The `C` class holds ANSI escape codes. `C.init()` is called at startup and on Windows attempts to enable ANSI processing via `os.system("")` (works on Win10+). If that fails, all color attributes are set to empty strings, producing uncolored but functional output. ## Auto-Migration On first run, if `decks/` is empty, the app scans `SCRIPT_DIR` for any `.json` files that look like valid decks and copies them into `decks/`. This handles the case where a user drops a deck file next to `aware.py` instead of inside the subdirectory.