aware/docs/DEVDOC.md
2026-05-02 21:22:18 -04:00

11 KiB

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

{
  "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)

{
  "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

{
  "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:

{
  "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:
    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:

{
  "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.

{
  "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.