Initial Commit
This commit is contained in:
commit
b4588fdf4f
2
.directory
Normal file
2
.directory
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
[Desktop Entry]
|
||||||
|
Icon=orange-folder-git
|
||||||
879
aware.py
Executable file
879
aware.py
Executable file
|
|
@ -0,0 +1,879 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
AWARE — Aviation Weighted Active Recall Engine
|
||||||
|
|
||||||
|
A cross-platform CLI study tool with spaced repetition and text-to-speech.
|
||||||
|
|
||||||
|
Features:
|
||||||
|
- 5-bucket weighted repetition (New/Missed -> Learning -> Known)
|
||||||
|
- Cross-platform TTS (Linux: espeak-ng, macOS: say, Windows: SAPI)
|
||||||
|
- Toggle voice on/off with 'v' at any time
|
||||||
|
- Multi-deck support: place .json files in the decks/ folder
|
||||||
|
- Per-deck progress saved to ~/.aware/progress/
|
||||||
|
- Category filtering, custom session lengths, missed-only review
|
||||||
|
- Optional config override: ~/.aware/config.json
|
||||||
|
- CAS message display and extended detail fields
|
||||||
|
|
||||||
|
Deck JSON schema (minimal):
|
||||||
|
{
|
||||||
|
"title": "My Study Deck",
|
||||||
|
"questions": [
|
||||||
|
{"question": "What is X?", "answer": "X is Y."}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
Full schema (with categories and refs):
|
||||||
|
{
|
||||||
|
"title": "My Study Deck",
|
||||||
|
"categories": [
|
||||||
|
{
|
||||||
|
"name": "Topic A",
|
||||||
|
"questions": [
|
||||||
|
{"id": 1, "question": "...", "answer": "...", "ref": "..."}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import hashlib
|
||||||
|
import os
|
||||||
|
import random
|
||||||
|
import re
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
# ── Configuration ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
DECKS_DIR = os.path.join(SCRIPT_DIR, "decks")
|
||||||
|
PROGRESS_DIR = os.path.expanduser("~/.aware/progress")
|
||||||
|
|
||||||
|
BUCKET_WEIGHTS = {1: 8, 2: 5, 3: 3, 4: 2, 5: 1}
|
||||||
|
BUCKET_LABELS = {1: "New / No Clue", 2: "Some", 3: "Half", 4: "Most", 5: "Nailed It"}
|
||||||
|
BUCKET_COLORS = {1: "RED", 2: "RED", 3: "YELLOW", 4: "GREEN", 5: "GREEN"}
|
||||||
|
|
||||||
|
# Grade input mapping: user input -> bucket assignment
|
||||||
|
# n and 1-3 set absolute bucket positions; y promotes by one
|
||||||
|
GRADE_TO_BUCKET = {"n": 1, "1": 2, "2": 3, "3": 4}
|
||||||
|
|
||||||
|
CONFIG_FILE = os.path.expanduser("~/.aware/config.json")
|
||||||
|
GLOSSARY_FILE = os.path.join(SCRIPT_DIR, "tts_glossary.json")
|
||||||
|
|
||||||
|
# ── Platform-specific TTS defaults ─────────────────────────────────────────
|
||||||
|
#
|
||||||
|
# Each backend is a dict with:
|
||||||
|
# cmd - executable name or path
|
||||||
|
# check - args to verify the tool exists (run with subprocess)
|
||||||
|
# build_args- function(text) -> full arg list for Popen
|
||||||
|
# label - human-readable name for status messages
|
||||||
|
# install - install hint shown when tool is missing
|
||||||
|
|
||||||
|
def _linux_tts():
|
||||||
|
return {
|
||||||
|
"cmd": "espeak-ng",
|
||||||
|
"check": ["espeak-ng", "--version"],
|
||||||
|
"build_args": lambda text: [
|
||||||
|
"espeak-ng", "-v", "en-us", "-s", "160", "--", text
|
||||||
|
],
|
||||||
|
"label": "espeak-ng",
|
||||||
|
"install": "sudo apt install espeak-ng",
|
||||||
|
}
|
||||||
|
|
||||||
|
def _macos_tts():
|
||||||
|
return {
|
||||||
|
"cmd": "say",
|
||||||
|
"check": ["which", "say"],
|
||||||
|
"build_args": lambda text: [
|
||||||
|
"say", "-v", "Alex", "-r", "180", text
|
||||||
|
],
|
||||||
|
"label": "macOS say",
|
||||||
|
"install": "say is built into macOS — check your PATH",
|
||||||
|
}
|
||||||
|
|
||||||
|
def _windows_tts():
|
||||||
|
# PowerShell's System.Speech is synchronous in-process but async from
|
||||||
|
# Python's perspective since we launch it as a subprocess. Killing the
|
||||||
|
# PowerShell process stops speech immediately.
|
||||||
|
def _build(text):
|
||||||
|
# Escape single quotes for PowerShell string literal
|
||||||
|
escaped = text.replace("'", "''")
|
||||||
|
ps_script = (
|
||||||
|
"Add-Type -AssemblyName System.Speech; "
|
||||||
|
"$s = New-Object System.Speech.Synthesis.SpeechSynthesizer; "
|
||||||
|
f"$s.Speak('{escaped}')"
|
||||||
|
)
|
||||||
|
return ["powershell", "-NoProfile", "-Command", ps_script]
|
||||||
|
|
||||||
|
return {
|
||||||
|
"cmd": "powershell",
|
||||||
|
"check": ["powershell", "-NoProfile", "-Command", "echo ok"],
|
||||||
|
"build_args": _build,
|
||||||
|
"label": "Windows SAPI",
|
||||||
|
"install": "PowerShell should be available on Windows by default",
|
||||||
|
}
|
||||||
|
|
||||||
|
def _detect_tts_backend():
|
||||||
|
"""Pick the right TTS backend for the current platform."""
|
||||||
|
if sys.platform == "darwin":
|
||||||
|
return _macos_tts()
|
||||||
|
elif sys.platform == "win32":
|
||||||
|
return _windows_tts()
|
||||||
|
else:
|
||||||
|
return _linux_tts()
|
||||||
|
|
||||||
|
# ── ANSI Colors ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
class C:
|
||||||
|
RESET = "\033[0m"
|
||||||
|
BOLD = "\033[1m"
|
||||||
|
DIM = "\033[2m"
|
||||||
|
GREEN = "\033[32m"
|
||||||
|
YELLOW = "\033[33m"
|
||||||
|
CYAN = "\033[36m"
|
||||||
|
RED = "\033[31m"
|
||||||
|
MAGENTA = "\033[35m"
|
||||||
|
WHITE = "\033[97m"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def init(cls):
|
||||||
|
"""Disable color codes on Windows without ANSI support."""
|
||||||
|
if sys.platform == "win32":
|
||||||
|
try:
|
||||||
|
os.system("") # enables ANSI on Win10+
|
||||||
|
except Exception:
|
||||||
|
# Fallback: strip all color
|
||||||
|
for attr in ("RESET", "BOLD", "DIM", "GREEN", "YELLOW",
|
||||||
|
"CYAN", "RED", "MAGENTA", "WHITE"):
|
||||||
|
setattr(cls, attr, "")
|
||||||
|
|
||||||
|
# ── TTS Config Override ───────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def _load_tts_config():
|
||||||
|
"""
|
||||||
|
Load optional user config from ~/.aware/config.json.
|
||||||
|
|
||||||
|
Supported keys:
|
||||||
|
tts_command - executable path/name (e.g. "piper", "espeak-ng")
|
||||||
|
tts_args - list of args; use {text} as placeholder for spoken text
|
||||||
|
e.g. ["--voice", "en_US", "{text}"]
|
||||||
|
|
||||||
|
If present, overrides the autodetected platform default.
|
||||||
|
"""
|
||||||
|
if not os.path.exists(CONFIG_FILE):
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
with open(CONFIG_FILE, "r") as f:
|
||||||
|
cfg = json.load(f)
|
||||||
|
if "tts_command" not in cfg:
|
||||||
|
return None
|
||||||
|
cmd = cfg["tts_command"]
|
||||||
|
args_template = cfg.get("tts_args", ["{text}"])
|
||||||
|
return {
|
||||||
|
"cmd": cmd,
|
||||||
|
"check": [cmd, "--version"],
|
||||||
|
"build_args": lambda text, _t=args_template, _c=cmd: (
|
||||||
|
[_c] + [a.replace("{text}", text) for a in _t]
|
||||||
|
),
|
||||||
|
"label": f"custom ({cmd})",
|
||||||
|
"install": f"Configured via {CONFIG_FILE}",
|
||||||
|
}
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# ── TTS Engine ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
class TTS:
|
||||||
|
"""
|
||||||
|
Cross-platform text-to-speech.
|
||||||
|
|
||||||
|
Autodetects:
|
||||||
|
Linux -> espeak-ng
|
||||||
|
macOS -> say
|
||||||
|
Windows -> PowerShell System.Speech (SAPI)
|
||||||
|
|
||||||
|
Override via ~/.aware/config.json:
|
||||||
|
{"tts_command": "piper", "tts_args": ["--model", "en", "{text}"]}
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.enabled = False
|
||||||
|
self._proc = None
|
||||||
|
|
||||||
|
# Try user config override first, then platform default
|
||||||
|
custom = _load_tts_config()
|
||||||
|
if custom:
|
||||||
|
self._backend = custom
|
||||||
|
else:
|
||||||
|
self._backend = _detect_tts_backend()
|
||||||
|
|
||||||
|
self.available = self._check_available()
|
||||||
|
self.label = self._backend["label"]
|
||||||
|
|
||||||
|
# Load pronunciation glossary
|
||||||
|
self._abbreviations = {}
|
||||||
|
self._symbols = {}
|
||||||
|
self._abbr_pattern = None
|
||||||
|
self._load_glossary()
|
||||||
|
|
||||||
|
def _load_glossary(self):
|
||||||
|
"""
|
||||||
|
Load pronunciation glossary from tts_glossary.json.
|
||||||
|
Falls back to a minimal built-in set if the file is missing.
|
||||||
|
"""
|
||||||
|
glossary_path = GLOSSARY_FILE
|
||||||
|
if os.path.exists(glossary_path):
|
||||||
|
try:
|
||||||
|
with open(glossary_path, "r") as f:
|
||||||
|
data = json.load(f)
|
||||||
|
self._abbreviations = data.get("abbreviations", {})
|
||||||
|
self._symbols = data.get("symbols", {})
|
||||||
|
except Exception:
|
||||||
|
self._abbreviations = {}
|
||||||
|
self._symbols = {}
|
||||||
|
|
||||||
|
# Compile a single regex that matches any abbreviation as a whole word.
|
||||||
|
# Sort by length descending so longer abbreviations match first
|
||||||
|
# (e.g. "KCAS" before "CAS", "OHPTS" before "HP").
|
||||||
|
if self._abbreviations:
|
||||||
|
sorted_abbrs = sorted(self._abbreviations.keys(),
|
||||||
|
key=len, reverse=True)
|
||||||
|
escaped = [re.escape(a) for a in sorted_abbrs]
|
||||||
|
self._abbr_pattern = re.compile(
|
||||||
|
r'\b(' + '|'.join(escaped) + r')\b'
|
||||||
|
)
|
||||||
|
|
||||||
|
def _check_available(self):
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
self._backend["check"],
|
||||||
|
capture_output=True, timeout=5
|
||||||
|
)
|
||||||
|
return result.returncode == 0
|
||||||
|
except (FileNotFoundError, subprocess.TimeoutExpired, OSError):
|
||||||
|
return False
|
||||||
|
|
||||||
|
def toggle(self):
|
||||||
|
if not self.available:
|
||||||
|
return False
|
||||||
|
self.enabled = not self.enabled
|
||||||
|
return self.enabled
|
||||||
|
|
||||||
|
def speak(self, text):
|
||||||
|
if not self.enabled or not self.available:
|
||||||
|
return
|
||||||
|
self.stop()
|
||||||
|
clean = self._clean_for_speech(text)
|
||||||
|
if not clean.strip():
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
args = self._backend["build_args"](clean)
|
||||||
|
# On Windows, hide the PowerShell window
|
||||||
|
kwargs = {}
|
||||||
|
if sys.platform == "win32":
|
||||||
|
si = subprocess.STARTUPINFO()
|
||||||
|
si.dwFlags |= subprocess.STARTF_USESHOWWINDOW
|
||||||
|
si.wShowWindow = 0 # SW_HIDE
|
||||||
|
kwargs["startupinfo"] = si
|
||||||
|
self._proc = subprocess.Popen(
|
||||||
|
args,
|
||||||
|
stdout=subprocess.DEVNULL,
|
||||||
|
stderr=subprocess.DEVNULL,
|
||||||
|
**kwargs
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
if self._proc and self._proc.poll() is None:
|
||||||
|
try:
|
||||||
|
self._proc.terminate()
|
||||||
|
self._proc.wait(timeout=2)
|
||||||
|
except Exception:
|
||||||
|
try:
|
||||||
|
self._proc.kill()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
self._proc = None
|
||||||
|
|
||||||
|
def install_hint(self):
|
||||||
|
return self._backend["install"]
|
||||||
|
|
||||||
|
def _clean_for_speech(self, text):
|
||||||
|
"""
|
||||||
|
Prepare text for TTS:
|
||||||
|
1. Strip ANSI escape codes
|
||||||
|
2. Replace abbreviations using word-boundary regex from glossary
|
||||||
|
3. Replace symbols (>=, °, etc.) from glossary
|
||||||
|
"""
|
||||||
|
# Strip ANSI
|
||||||
|
text = re.sub(r'\033\[[0-9;]*m', '', text)
|
||||||
|
|
||||||
|
# Replace abbreviations at word boundaries only
|
||||||
|
if self._abbr_pattern:
|
||||||
|
text = self._abbr_pattern.sub(
|
||||||
|
lambda m: self._abbreviations[m.group(0)], text
|
||||||
|
)
|
||||||
|
|
||||||
|
# Replace symbols (order matters: >= before >)
|
||||||
|
for sym, replacement in sorted(self._symbols.items(),
|
||||||
|
key=lambda x: len(x[0]),
|
||||||
|
reverse=True):
|
||||||
|
text = text.replace(sym, replacement)
|
||||||
|
|
||||||
|
return text
|
||||||
|
|
||||||
|
# ── Deck Discovery & Loading ───────────────────────────────────────────────
|
||||||
|
|
||||||
|
def discover_decks():
|
||||||
|
decks = []
|
||||||
|
if os.path.isdir(DECKS_DIR):
|
||||||
|
for f in sorted(os.listdir(DECKS_DIR)):
|
||||||
|
if f.endswith(".json"):
|
||||||
|
path = os.path.join(DECKS_DIR, f)
|
||||||
|
info = _peek_deck(path)
|
||||||
|
if info:
|
||||||
|
decks.append(info)
|
||||||
|
return decks
|
||||||
|
|
||||||
|
|
||||||
|
def _peek_deck(path):
|
||||||
|
try:
|
||||||
|
with open(path, "r") as f:
|
||||||
|
data = json.load(f)
|
||||||
|
title = data.get("title", Path(path).stem)
|
||||||
|
count = _count_questions(data)
|
||||||
|
if count == 0:
|
||||||
|
return None
|
||||||
|
return {"path": path, "title": title, "count": count, "filename": os.path.basename(path)}
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _count_questions(data):
|
||||||
|
if "categories" in data:
|
||||||
|
return sum(len(cat.get("questions", [])) for cat in data["categories"])
|
||||||
|
elif "questions" in data:
|
||||||
|
return len(data["questions"])
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def load_deck(path):
|
||||||
|
"""
|
||||||
|
Load a deck from JSON. Supports two formats:
|
||||||
|
1. Categorized: {"categories": [{"name": "...", "questions": [...]}]}
|
||||||
|
2. Flat: {"questions": [{"question": "...", "answer": "..."}]}
|
||||||
|
|
||||||
|
Returns (question_list, category_names, title).
|
||||||
|
"""
|
||||||
|
with open(path, "r") as f:
|
||||||
|
data = json.load(f)
|
||||||
|
|
||||||
|
title = data.get("title", Path(path).stem)
|
||||||
|
questions = []
|
||||||
|
categories = []
|
||||||
|
auto_id = 1
|
||||||
|
|
||||||
|
if "categories" in data:
|
||||||
|
for cat in data["categories"]:
|
||||||
|
cat_name = cat.get("name", "Uncategorized")
|
||||||
|
categories.append(cat_name)
|
||||||
|
for q in cat.get("questions", []):
|
||||||
|
if "id" not in q:
|
||||||
|
q["id"] = auto_id
|
||||||
|
auto_id += 1
|
||||||
|
q.setdefault("category", cat_name)
|
||||||
|
q.setdefault("ref", "")
|
||||||
|
questions.append(q)
|
||||||
|
elif "questions" in data:
|
||||||
|
categories.append("General")
|
||||||
|
for q in data["questions"]:
|
||||||
|
if "id" not in q:
|
||||||
|
q["id"] = auto_id
|
||||||
|
auto_id += 1
|
||||||
|
q.setdefault("category", "General")
|
||||||
|
q.setdefault("ref", "")
|
||||||
|
questions.append(q)
|
||||||
|
|
||||||
|
return questions, categories, title
|
||||||
|
|
||||||
|
# ── Progress Persistence ───────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def _progress_path(deck_path):
|
||||||
|
deck_hash = hashlib.md5(os.path.abspath(deck_path).encode()).hexdigest()[:12]
|
||||||
|
deck_name = Path(deck_path).stem
|
||||||
|
os.makedirs(PROGRESS_DIR, exist_ok=True)
|
||||||
|
return os.path.join(PROGRESS_DIR, f"{deck_name}_{deck_hash}.json")
|
||||||
|
|
||||||
|
|
||||||
|
def load_progress(deck_path):
|
||||||
|
ppath = _progress_path(deck_path)
|
||||||
|
if os.path.exists(ppath):
|
||||||
|
with open(ppath, "r") as f:
|
||||||
|
return json.load(f)
|
||||||
|
return {"buckets": {}, "stats": {"total_sessions": 0, "total_correct": 0, "total_wrong": 0}}
|
||||||
|
|
||||||
|
|
||||||
|
def save_progress(deck_path, progress):
|
||||||
|
ppath = _progress_path(deck_path)
|
||||||
|
with open(ppath, "w") as f:
|
||||||
|
json.dump(progress, f, indent=2)
|
||||||
|
|
||||||
|
# ── Bucket Management ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def get_bucket(progress, qid):
|
||||||
|
return progress["buckets"].get(str(qid), 1)
|
||||||
|
|
||||||
|
|
||||||
|
def set_bucket(progress, qid, bucket):
|
||||||
|
"""Set a question to a specific bucket (1-5)."""
|
||||||
|
progress["buckets"][str(qid)] = max(1, min(bucket, 5))
|
||||||
|
|
||||||
|
|
||||||
|
def grade_question(progress, qid, grade):
|
||||||
|
"""
|
||||||
|
Apply a grade to a question and return the new bucket.
|
||||||
|
|
||||||
|
Grades:
|
||||||
|
'n' -> bucket 1 (absolute)
|
||||||
|
'1' -> bucket 2 (absolute)
|
||||||
|
'2' -> bucket 3 (absolute)
|
||||||
|
'3' -> bucket 4 (absolute)
|
||||||
|
'y' -> promote by 1, max 5 (relative)
|
||||||
|
"""
|
||||||
|
if grade in GRADE_TO_BUCKET:
|
||||||
|
new_bucket = GRADE_TO_BUCKET[grade]
|
||||||
|
elif grade == "y":
|
||||||
|
new_bucket = min(get_bucket(progress, qid) + 1, 5)
|
||||||
|
else:
|
||||||
|
return get_bucket(progress, qid)
|
||||||
|
|
||||||
|
set_bucket(progress, qid, new_bucket)
|
||||||
|
return new_bucket
|
||||||
|
|
||||||
|
|
||||||
|
def pick_weighted(questions, progress):
|
||||||
|
weights = [BUCKET_WEIGHTS[get_bucket(progress, q["id"])] for q in questions]
|
||||||
|
return random.choices(questions, weights=weights, k=1)[0]
|
||||||
|
|
||||||
|
# ── Display Helpers ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def term_width():
|
||||||
|
try:
|
||||||
|
return min(os.get_terminal_size().columns, 80)
|
||||||
|
except OSError:
|
||||||
|
return 80
|
||||||
|
|
||||||
|
|
||||||
|
def clear_screen():
|
||||||
|
os.system("clear" if os.name != "nt" else "cls")
|
||||||
|
|
||||||
|
|
||||||
|
def print_header(text, tts=None):
|
||||||
|
width = term_width()
|
||||||
|
print(f"\n{C.CYAN}{'=' * width}{C.RESET}")
|
||||||
|
line = f" {text}"
|
||||||
|
if tts:
|
||||||
|
if tts.enabled:
|
||||||
|
state = f"{C.GREEN}ON ({tts.label}){C.RESET}"
|
||||||
|
else:
|
||||||
|
state = f"{C.DIM}OFF{C.RESET}"
|
||||||
|
line += f" {C.DIM}|{C.RESET} Voice: {state}"
|
||||||
|
print(f"{C.BOLD}{C.WHITE}{line}{C.RESET}")
|
||||||
|
print(f"{C.CYAN}{'=' * width}{C.RESET}")
|
||||||
|
|
||||||
|
|
||||||
|
def print_question(q, num, total, progress):
|
||||||
|
bucket = get_bucket(progress, q["id"])
|
||||||
|
bucket_label = BUCKET_LABELS[bucket]
|
||||||
|
color_map = {1: C.RED, 2: C.RED, 3: C.YELLOW, 4: C.GREEN, 5: C.GREEN}
|
||||||
|
bucket_color = color_map[bucket]
|
||||||
|
|
||||||
|
cat = q.get("category", "")
|
||||||
|
cat_str = f" Category: {C.CYAN}{cat}{C.RESET}" if cat and cat != "General" else ""
|
||||||
|
|
||||||
|
print(f"\n{C.DIM}[{num}/{total}]{cat_str}"
|
||||||
|
f" {C.DIM}| Bucket: {bucket_color}{bucket_label}{C.RESET}")
|
||||||
|
|
||||||
|
# Display CAS messages if present
|
||||||
|
cas = q.get("cas")
|
||||||
|
if cas:
|
||||||
|
if isinstance(cas, str):
|
||||||
|
cas = [cas]
|
||||||
|
print(f"\n {C.BOLD}{C.WHITE}CAS:{C.RESET}")
|
||||||
|
for msg in cas:
|
||||||
|
print(f" {msg}")
|
||||||
|
|
||||||
|
print(f"\n{C.BOLD}{C.WHITE} Q: {q['question']}{C.RESET}\n")
|
||||||
|
|
||||||
|
|
||||||
|
def print_answer(q):
|
||||||
|
print(f"{C.GREEN}{C.BOLD} A: {C.RESET}{C.GREEN}{q['answer']}{C.RESET}")
|
||||||
|
if q.get("ref"):
|
||||||
|
print(f"{C.DIM} Ref: {q['ref']}{C.RESET}")
|
||||||
|
if q.get("detail"):
|
||||||
|
print(f"\n {C.CYAN}{C.BOLD}Detail:{C.RESET}")
|
||||||
|
print(f" {C.CYAN}{q['detail']}{C.RESET}")
|
||||||
|
|
||||||
|
|
||||||
|
def print_session_stats(grades, total):
|
||||||
|
width = term_width()
|
||||||
|
nailed = grades.get("y", 0)
|
||||||
|
missed = grades.get("n", 0)
|
||||||
|
partial = grades.get("1", 0) + grades.get("2", 0) + grades.get("3", 0)
|
||||||
|
# Score: y=100%, 3=75%, 2=50%, 1=25%, n=0%
|
||||||
|
score_sum = (nailed * 100 + grades.get("3", 0) * 75 +
|
||||||
|
grades.get("2", 0) * 50 + grades.get("1", 0) * 25)
|
||||||
|
avg = (score_sum / total) if total > 0 else 0
|
||||||
|
|
||||||
|
print(f"\n{C.CYAN}{'-' * width}{C.RESET}")
|
||||||
|
print(f" {C.BOLD}Session Results ({total} questions):{C.RESET}")
|
||||||
|
print(f" {C.GREEN}Nailed (y): {nailed}{C.RESET} | "
|
||||||
|
f"{C.YELLOW}Partial (1/2/3): {partial}{C.RESET} | "
|
||||||
|
f"{C.RED}Missed (n): {missed}{C.RESET} | "
|
||||||
|
f"{C.BOLD}Avg: {avg:.0f}%{C.RESET}")
|
||||||
|
print(f"{C.CYAN}{'-' * width}{C.RESET}")
|
||||||
|
|
||||||
|
|
||||||
|
def print_overall_stats(progress, questions):
|
||||||
|
buckets = {i: 0 for i in range(1, 6)}
|
||||||
|
for q in questions:
|
||||||
|
buckets[get_bucket(progress, q["id"])] += 1
|
||||||
|
total = len(questions)
|
||||||
|
|
||||||
|
print(f"\n {C.BOLD}Progress ({total} questions):{C.RESET}")
|
||||||
|
print(f" {C.RED}Bucket 1 (No Clue): {buckets[1]:>4} ({buckets[1]/total*100:5.1f}%){C.RESET}")
|
||||||
|
print(f" {C.RED}Bucket 2 (Some): {buckets[2]:>4} ({buckets[2]/total*100:5.1f}%){C.RESET}")
|
||||||
|
print(f" {C.YELLOW}Bucket 3 (Half): {buckets[3]:>4} ({buckets[3]/total*100:5.1f}%){C.RESET}")
|
||||||
|
print(f" {C.GREEN}Bucket 4 (Most): {buckets[4]:>4} ({buckets[4]/total*100:5.1f}%){C.RESET}")
|
||||||
|
print(f" {C.GREEN}Bucket 5 (Nailed): {buckets[5]:>4} ({buckets[5]/total*100:5.1f}%){C.RESET}")
|
||||||
|
|
||||||
|
s = progress["stats"]
|
||||||
|
if s["total_sessions"] > 0:
|
||||||
|
lifetime = s.get("total_correct", 0) + s.get("total_wrong", 0) + s.get("total_partial", 0)
|
||||||
|
if lifetime > 0:
|
||||||
|
lifetime_pct = (s.get("total_correct", 0) / lifetime * 100)
|
||||||
|
print(f"\n {C.DIM}Sessions: {s['total_sessions']} | "
|
||||||
|
f"Lifetime: {s.get('total_correct',0)} nailed / "
|
||||||
|
f"{s.get('total_partial',0)} partial / "
|
||||||
|
f"{s.get('total_wrong',0)} missed ({lifetime_pct:.0f}% nailed){C.RESET}")
|
||||||
|
|
||||||
|
# ── Category Selection ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def select_categories(categories):
|
||||||
|
print(f"\n {C.BOLD}Categories:{C.RESET}")
|
||||||
|
print(f" {C.CYAN}0{C.RESET}) All categories")
|
||||||
|
for i, cat in enumerate(categories, 1):
|
||||||
|
print(f" {C.CYAN}{i}{C.RESET}) {cat}")
|
||||||
|
|
||||||
|
print(f"\n Enter numbers separated by commas (e.g. 1,3,5) or 0 for all:")
|
||||||
|
try:
|
||||||
|
raw = input(f" {C.BOLD}>{C.RESET} ").strip()
|
||||||
|
except (EOFError, KeyboardInterrupt):
|
||||||
|
return None
|
||||||
|
|
||||||
|
if not raw or raw == "0":
|
||||||
|
return None
|
||||||
|
|
||||||
|
selected = []
|
||||||
|
for part in raw.split(","):
|
||||||
|
part = part.strip()
|
||||||
|
if part.isdigit():
|
||||||
|
idx = int(part)
|
||||||
|
if 1 <= idx <= len(categories):
|
||||||
|
selected.append(categories[idx - 1])
|
||||||
|
|
||||||
|
return selected if selected else None
|
||||||
|
|
||||||
|
# ── Quiz Loop ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def run_quiz(questions, deck_path, progress, count, title, tts):
|
||||||
|
grades = {"n": 0, "1": 0, "2": 0, "3": 0, "y": 0}
|
||||||
|
|
||||||
|
for i in range(1, count + 1):
|
||||||
|
clear_screen()
|
||||||
|
print_header(title, tts)
|
||||||
|
pick = pick_weighted(questions, progress)
|
||||||
|
print_question(pick, i, count, progress)
|
||||||
|
|
||||||
|
# Build spoken text with CAS context if present
|
||||||
|
speak_text = ""
|
||||||
|
cas = pick.get("cas")
|
||||||
|
if cas:
|
||||||
|
if isinstance(cas, str):
|
||||||
|
cas = [cas]
|
||||||
|
speak_text = "CAS: " + ", ".join(cas) + ". "
|
||||||
|
speak_text += pick["question"]
|
||||||
|
|
||||||
|
tts.speak(speak_text)
|
||||||
|
|
||||||
|
input(f" {C.DIM}[Press Enter to reveal answer]{C.RESET}")
|
||||||
|
tts.stop()
|
||||||
|
print_answer(pick)
|
||||||
|
|
||||||
|
print(f"\n {C.BOLD}How'd you do?{C.RESET}")
|
||||||
|
print(f" {C.RED}n{C.RESET} - didn't get it "
|
||||||
|
f"{C.YELLOW}1{C.RESET} - some "
|
||||||
|
f"{C.YELLOW}2{C.RESET} - half "
|
||||||
|
f"{C.YELLOW}3{C.RESET} - most "
|
||||||
|
f"{C.GREEN}y{C.RESET} - nailed it")
|
||||||
|
print(f" {C.MAGENTA}v{C.RESET} - voice toggle "
|
||||||
|
f"{C.DIM}q - quit{C.RESET}")
|
||||||
|
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
resp = input(f" {C.BOLD}>{C.RESET} ").strip().lower()
|
||||||
|
except (EOFError, KeyboardInterrupt):
|
||||||
|
resp = "q"
|
||||||
|
|
||||||
|
if resp in ("y", "yes", "+"):
|
||||||
|
grade = "y"
|
||||||
|
elif resp in ("n", "no", "-", ""):
|
||||||
|
grade = "n"
|
||||||
|
elif resp in ("1", "2", "3"):
|
||||||
|
grade = resp
|
||||||
|
elif resp in ("v", "voice"):
|
||||||
|
new_state = tts.toggle()
|
||||||
|
label = f"{C.GREEN}ON{C.RESET}" if new_state else f"{C.DIM}OFF{C.RESET}"
|
||||||
|
print(f" {C.MAGENTA}Voice: {label}{C.RESET}")
|
||||||
|
if new_state:
|
||||||
|
tts.speak(speak_text)
|
||||||
|
continue
|
||||||
|
elif resp in ("q", "quit"):
|
||||||
|
tts.stop()
|
||||||
|
print_session_stats(grades, sum(grades.values()))
|
||||||
|
save_progress(deck_path, progress)
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
print(f" {C.DIM}Enter n/-, 1, 2, 3, y/+, v, or q{C.RESET}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Apply grade
|
||||||
|
grades[grade] += 1
|
||||||
|
new_bucket = grade_question(progress, pick["id"], grade)
|
||||||
|
new_label = BUCKET_LABELS[new_bucket]
|
||||||
|
|
||||||
|
if grade == "y":
|
||||||
|
progress["stats"]["total_correct"] = progress["stats"].get("total_correct", 0) + 1
|
||||||
|
print(f" {C.GREEN}+ Nailed it — {new_label}{C.RESET}")
|
||||||
|
elif grade == "n":
|
||||||
|
progress["stats"]["total_wrong"] = progress["stats"].get("total_wrong", 0) + 1
|
||||||
|
print(f" {C.RED}x Missed — {new_label}{C.RESET}")
|
||||||
|
else:
|
||||||
|
progress["stats"]["total_partial"] = progress["stats"].get("total_partial", 0) + 1
|
||||||
|
print(f" {C.YELLOW}~ Partial — {new_label}{C.RESET}")
|
||||||
|
break
|
||||||
|
|
||||||
|
time.sleep(0.6)
|
||||||
|
|
||||||
|
progress["stats"]["total_sessions"] += 1
|
||||||
|
save_progress(deck_path, progress)
|
||||||
|
tts.stop()
|
||||||
|
|
||||||
|
clear_screen()
|
||||||
|
print_header("Session Complete", tts)
|
||||||
|
print_session_stats(grades, count)
|
||||||
|
print_overall_stats(progress, questions)
|
||||||
|
|
||||||
|
|
||||||
|
def review_missed(questions, deck_path, progress, title, tts):
|
||||||
|
missed = [q for q in questions if get_bucket(progress, q["id"]) == 1]
|
||||||
|
if not missed:
|
||||||
|
print(f"\n {C.GREEN}No questions in Bucket 1 — nothing to review.{C.RESET}")
|
||||||
|
return
|
||||||
|
|
||||||
|
random.shuffle(missed)
|
||||||
|
print(f"\n {C.BOLD}Reviewing {len(missed)} missed/new questions...{C.RESET}")
|
||||||
|
time.sleep(1)
|
||||||
|
run_quiz(missed, deck_path, progress, min(len(missed), 50), title, tts)
|
||||||
|
|
||||||
|
# ── Deck Selection ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def select_deck():
|
||||||
|
decks = discover_decks()
|
||||||
|
|
||||||
|
if not decks:
|
||||||
|
print(f"\n {C.RED}No decks found.{C.RESET}")
|
||||||
|
print(f" Place .json deck files in: {C.CYAN}{DECKS_DIR}/{C.RESET}")
|
||||||
|
print(f"\n Minimal deck format:")
|
||||||
|
print(f' {C.DIM}{{"title": "My Deck", "questions": '
|
||||||
|
f'[{{"question": "Q?", "answer": "A."}}]}}{C.RESET}')
|
||||||
|
return None
|
||||||
|
|
||||||
|
if len(decks) == 1:
|
||||||
|
return decks[0]["path"]
|
||||||
|
|
||||||
|
print(f"\n {C.BOLD}Available Decks:{C.RESET}\n")
|
||||||
|
for i, d in enumerate(decks, 1):
|
||||||
|
print(f" {C.CYAN}{i}{C.RESET}) {d['title']} "
|
||||||
|
f"{C.DIM}({d['count']} questions — {d['filename']}){C.RESET}")
|
||||||
|
|
||||||
|
print()
|
||||||
|
try:
|
||||||
|
raw = input(f" Select deck {C.BOLD}>{C.RESET} ").strip()
|
||||||
|
except (EOFError, KeyboardInterrupt):
|
||||||
|
return None
|
||||||
|
|
||||||
|
if raw.isdigit():
|
||||||
|
idx = int(raw)
|
||||||
|
if 1 <= idx <= len(decks):
|
||||||
|
return decks[idx - 1]["path"]
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
# ── Main Menu ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def deck_menu(deck_path, tts):
|
||||||
|
questions, categories, title = load_deck(deck_path)
|
||||||
|
progress = load_progress(deck_path)
|
||||||
|
|
||||||
|
while True:
|
||||||
|
clear_screen()
|
||||||
|
print_header(title, tts)
|
||||||
|
print_overall_stats(progress, questions)
|
||||||
|
|
||||||
|
multi_cat = len(categories) > 1
|
||||||
|
|
||||||
|
print(f"\n {C.BOLD}Menu:{C.RESET}")
|
||||||
|
print(f" {C.CYAN}1{C.RESET}) Quick 10")
|
||||||
|
print(f" {C.CYAN}2{C.RESET}) Session 25")
|
||||||
|
print(f" {C.CYAN}3{C.RESET}) Full 50")
|
||||||
|
print(f" {C.CYAN}4{C.RESET}) Custom count")
|
||||||
|
if multi_cat:
|
||||||
|
print(f" {C.CYAN}5{C.RESET}) By category")
|
||||||
|
print(f" {C.CYAN}6{C.RESET}) Review missed only (Bucket 1)")
|
||||||
|
voice_state = f"{C.GREEN}ON{C.RESET}" if tts.enabled else f"{C.DIM}OFF{C.RESET}"
|
||||||
|
print(f" {C.MAGENTA}v{C.RESET}) Toggle voice ({voice_state})")
|
||||||
|
print(f" {C.CYAN}7{C.RESET}) Reset deck progress")
|
||||||
|
print(f" {C.CYAN}d{C.RESET}) Switch deck")
|
||||||
|
print(f" {C.CYAN}q{C.RESET}) Quit")
|
||||||
|
|
||||||
|
try:
|
||||||
|
choice = input(f"\n {C.BOLD}>{C.RESET} ").strip().lower()
|
||||||
|
except (EOFError, KeyboardInterrupt):
|
||||||
|
break
|
||||||
|
|
||||||
|
if choice == "1":
|
||||||
|
run_quiz(questions, deck_path, progress,
|
||||||
|
min(10, len(questions)), title, tts)
|
||||||
|
elif choice == "2":
|
||||||
|
run_quiz(questions, deck_path, progress,
|
||||||
|
min(25, len(questions)), title, tts)
|
||||||
|
elif choice == "3":
|
||||||
|
run_quiz(questions, deck_path, progress,
|
||||||
|
min(50, len(questions)), title, tts)
|
||||||
|
elif choice == "4":
|
||||||
|
try:
|
||||||
|
n = int(input(f" How many questions? (1-{len(questions)}): ").strip())
|
||||||
|
n = max(1, min(n, len(questions)))
|
||||||
|
except (ValueError, EOFError, KeyboardInterrupt):
|
||||||
|
continue
|
||||||
|
run_quiz(questions, deck_path, progress, n, title, tts)
|
||||||
|
elif choice == "5" and multi_cat:
|
||||||
|
selected = select_categories(categories)
|
||||||
|
filtered = ([q for q in questions if q["category"] in selected]
|
||||||
|
if selected else questions)
|
||||||
|
if not filtered:
|
||||||
|
print(f" {C.RED}No questions matched.{C.RESET}")
|
||||||
|
time.sleep(1)
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
n = int(input(f" How many? (1-{len(filtered)}, default 10): "
|
||||||
|
).strip() or "10")
|
||||||
|
n = max(1, min(n, len(filtered)))
|
||||||
|
except (ValueError, EOFError, KeyboardInterrupt):
|
||||||
|
continue
|
||||||
|
run_quiz(filtered, deck_path, progress, n, title, tts)
|
||||||
|
elif choice == "6":
|
||||||
|
review_missed(questions, deck_path, progress, title, tts)
|
||||||
|
elif choice in ("v", "voice"):
|
||||||
|
new_state = tts.toggle()
|
||||||
|
if not tts.available:
|
||||||
|
print(f" {C.RED}TTS not available ({tts.label}). "
|
||||||
|
f"{tts.install_hint()}{C.RESET}")
|
||||||
|
time.sleep(2)
|
||||||
|
else:
|
||||||
|
label = (f"{C.GREEN}ON{C.RESET}" if new_state
|
||||||
|
else f"{C.DIM}OFF{C.RESET}")
|
||||||
|
print(f" {C.MAGENTA}Voice: {label}{C.RESET}")
|
||||||
|
time.sleep(0.5)
|
||||||
|
continue
|
||||||
|
elif choice == "7":
|
||||||
|
try:
|
||||||
|
confirm = input(
|
||||||
|
f" {C.RED}Reset progress for this deck? "
|
||||||
|
f"(type YES): {C.RESET}"
|
||||||
|
).strip()
|
||||||
|
except (EOFError, KeyboardInterrupt):
|
||||||
|
continue
|
||||||
|
if confirm == "YES":
|
||||||
|
progress = {
|
||||||
|
"buckets": {},
|
||||||
|
"stats": {"total_sessions": 0,
|
||||||
|
"total_correct": 0,
|
||||||
|
"total_wrong": 0}
|
||||||
|
}
|
||||||
|
save_progress(deck_path, progress)
|
||||||
|
print(f" {C.GREEN}Progress reset.{C.RESET}")
|
||||||
|
time.sleep(1)
|
||||||
|
continue
|
||||||
|
elif choice == "d":
|
||||||
|
save_progress(deck_path, progress)
|
||||||
|
return "switch"
|
||||||
|
elif choice in ("q", "quit"):
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
continue
|
||||||
|
|
||||||
|
input(f"\n {C.DIM}[Press Enter to return to menu]{C.RESET}")
|
||||||
|
|
||||||
|
save_progress(deck_path, progress)
|
||||||
|
return "quit"
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
C.init()
|
||||||
|
os.makedirs(DECKS_DIR, exist_ok=True)
|
||||||
|
|
||||||
|
# Auto-migrate legacy file from script directory into decks/
|
||||||
|
if not discover_decks():
|
||||||
|
for f in os.listdir(SCRIPT_DIR):
|
||||||
|
if f.endswith(".json") and f != "package.json":
|
||||||
|
src = os.path.join(SCRIPT_DIR, f)
|
||||||
|
info = _peek_deck(src)
|
||||||
|
if info:
|
||||||
|
print(f" {C.YELLOW}Moving {f} into decks/ ...{C.RESET}")
|
||||||
|
shutil.copy2(src, os.path.join(DECKS_DIR, f))
|
||||||
|
|
||||||
|
if not discover_decks():
|
||||||
|
print(f"\n {C.RED}No decks found.{C.RESET}")
|
||||||
|
print(f" Place .json deck files in: {C.CYAN}{DECKS_DIR}/{C.RESET}")
|
||||||
|
print(f"\n Minimal format:")
|
||||||
|
print(f' {C.DIM}{{"title": "My Deck", '
|
||||||
|
f'"questions": [{{"question": "Q?", "answer": "A."}}]}}{C.RESET}\n')
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
tts = TTS()
|
||||||
|
|
||||||
|
while True:
|
||||||
|
clear_screen()
|
||||||
|
print_header("AWARE — Aviation Weighted Active Recall Engine", tts)
|
||||||
|
|
||||||
|
deck_path = select_deck()
|
||||||
|
if deck_path is None:
|
||||||
|
break
|
||||||
|
|
||||||
|
result = deck_menu(deck_path, tts)
|
||||||
|
if result == "quit":
|
||||||
|
break
|
||||||
|
|
||||||
|
tts.stop()
|
||||||
|
print(f"\n {C.DIM}Progress saved. Fly safe.{C.RESET}\n")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
74
aware_example_deck.json
Normal file
74
aware_example_deck.json
Normal file
|
|
@ -0,0 +1,74 @@
|
||||||
|
{
|
||||||
|
"title": "AWARE Example Deck",
|
||||||
|
"categories": [
|
||||||
|
{
|
||||||
|
"name": "Aircraft General",
|
||||||
|
"questions": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"question": "What is the difference between a \u001b[1;31;40mRED\u001b[0m CAS message and an \u001b[1;33;40mAMBER\u001b[0m CAS message?",
|
||||||
|
"answer": "\u001b[1;31;40mRED\u001b[0m requires immediate flight crew awareness and immediate flight crew response.\n\u001b[1;33;40mAMBER\u001b[0m requires immediate flight crew awareness and subsequent flight crew response.",
|
||||||
|
"ref": "AFM, CAS Message Philosophy",
|
||||||
|
"detail": "The key distinction is timing of the response. RED means act NOW — these are conditions like fire, engine failure, or rapid decompression where delay increases risk. AMBER means assess and then respond — the situation needs attention but allows time to reference checklists and coordinate crew actions."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"question": "What does the \u001b[1;36;40mREADY TO OPEN\u001b[0m light indicate in reference to the opening of the MED?",
|
||||||
|
"answer": "1. Parking Brake is SET.\n2. Cabin Diff. Press supports a comfortable opening.",
|
||||||
|
"ref": "AFM / Exterior Preflight Inspection; PAS / Aircraft General"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3,
|
||||||
|
"question": "During power up you notice that DU #2 has a Red X. What would you do? Could you perform the same procedure if this occurred in FLIGHT?",
|
||||||
|
"answer": "Switch DU #2 to ALT on OHPTS.\nNo, you cannot perform this procedure in flight.",
|
||||||
|
"ref": "MEL / NAVIGATION",
|
||||||
|
"detail": "The ALT switch on the OHPTS reassigns the display to an alternate video source. This is a ground-only action because changing display routing in flight could cause momentary loss of critical flight information. DU 3 is the only display unit that can be failed for dispatch."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 4,
|
||||||
|
"question": "What controls automatic functions on the aircraft?",
|
||||||
|
"answer": "Data Concentration Network (DCN) and Secondary Power Distribution System (SPDS).",
|
||||||
|
"ref": "PAS / Aircraft General"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Landing Gear & Brakes",
|
||||||
|
"questions": [
|
||||||
|
{
|
||||||
|
"id": 5,
|
||||||
|
"question": "What are you going to do?",
|
||||||
|
"answer": "AFM Landing Gear Failure to Retract checklist.",
|
||||||
|
"ref": "AFM Landing Gear Failure to Retract checklist",
|
||||||
|
"cas": [
|
||||||
|
"\u001b[1;33;40mMain Gear Not Up, L-R\u001b[0m",
|
||||||
|
"\u001b[1;33;40mNose Gear Not Up\u001b[0m"
|
||||||
|
],
|
||||||
|
"detail": "These AMBER CAS messages appear after selecting gear up with a positive rate of climb. The gear has failed to retract — follow the AFM checklist. Do not attempt to cycle the gear without consulting the checklist first."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 6,
|
||||||
|
"question": "If Tiller Steering Fails, what type of rudder pedal authority would you expect?",
|
||||||
|
"answer": "16° (AFM) / 17° (PAS)",
|
||||||
|
"ref": "AFM, Tiller Steering Fail; PAS / Landing Gear and Brakes",
|
||||||
|
"cas": "\u001b[1;33;40mTiller Steering Fail\u001b[0m",
|
||||||
|
"detail": "Normal tiller authority is 82°, and normal pedal steering is 7°. With tiller failure, the pedals get increased authority (16-17°) as a compensating mode. This is enough for taxi but tight turns will require more planning."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 7,
|
||||||
|
"question": "How does touchdown protection work?",
|
||||||
|
"answer": "Zero brake pressure is applied until:\n1. Wheel Speed > 70 kts\nOR\n2. WOW plus 5 seconds",
|
||||||
|
"ref": "PAS / Landing Gear and Brakes"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 8,
|
||||||
|
"question": "Is there a CAS message to identify the landing gear at the LGCMP is not in the NORM mode?",
|
||||||
|
"answer": "Yes: \u001b[1;33;40mLGCU Maintenance Mode\u001b[0m",
|
||||||
|
"ref": "AFM, LGCU Maintenance Mode",
|
||||||
|
"cas": "\u001b[1;33;40mLGCU Maintenance Mode\u001b[0m",
|
||||||
|
"detail": "After opening gear doors for preflight, you must close all gear doors AND select Normal on the LGCMP. If you forget, the plane stays in MX mode and the gear will not retract on departure. This CAS message is your safety net."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
74
decks/aware_example_deck.json
Normal file
74
decks/aware_example_deck.json
Normal file
|
|
@ -0,0 +1,74 @@
|
||||||
|
{
|
||||||
|
"title": "AWARE Example Deck",
|
||||||
|
"categories": [
|
||||||
|
{
|
||||||
|
"name": "Aircraft General",
|
||||||
|
"questions": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"question": "What is the difference between a \u001b[1;31;40mRED\u001b[0m CAS message and an \u001b[1;33;40mAMBER\u001b[0m CAS message?",
|
||||||
|
"answer": "\u001b[1;31;40mRED\u001b[0m requires immediate flight crew awareness and immediate flight crew response.\n\u001b[1;33;40mAMBER\u001b[0m requires immediate flight crew awareness and subsequent flight crew response.",
|
||||||
|
"ref": "AFM, CAS Message Philosophy",
|
||||||
|
"detail": "The key distinction is timing of the response. RED means act NOW — these are conditions like fire, engine failure, or rapid decompression where delay increases risk. AMBER means assess and then respond — the situation needs attention but allows time to reference checklists and coordinate crew actions."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"question": "What does the \u001b[1;36;40mREADY TO OPEN\u001b[0m light indicate in reference to the opening of the MED?",
|
||||||
|
"answer": "1. Parking Brake is SET.\n2. Cabin Diff. Press supports a comfortable opening.",
|
||||||
|
"ref": "AFM / Exterior Preflight Inspection; PAS / Aircraft General"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3,
|
||||||
|
"question": "During power up you notice that DU #2 has a Red X. What would you do? Could you perform the same procedure if this occurred in FLIGHT?",
|
||||||
|
"answer": "Switch DU #2 to ALT on OHPTS.\nNo, you cannot perform this procedure in flight.",
|
||||||
|
"ref": "MEL / NAVIGATION",
|
||||||
|
"detail": "The ALT switch on the OHPTS reassigns the display to an alternate video source. This is a ground-only action because changing display routing in flight could cause momentary loss of critical flight information. DU 3 is the only display unit that can be failed for dispatch."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 4,
|
||||||
|
"question": "What controls automatic functions on the aircraft?",
|
||||||
|
"answer": "Data Concentration Network (DCN) and Secondary Power Distribution System (SPDS).",
|
||||||
|
"ref": "PAS / Aircraft General"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Landing Gear & Brakes",
|
||||||
|
"questions": [
|
||||||
|
{
|
||||||
|
"id": 5,
|
||||||
|
"question": "What are you going to do?",
|
||||||
|
"answer": "AFM Landing Gear Failure to Retract checklist.",
|
||||||
|
"ref": "AFM Landing Gear Failure to Retract checklist",
|
||||||
|
"cas": [
|
||||||
|
"\u001b[1;33;40mMain Gear Not Up, L-R\u001b[0m",
|
||||||
|
"\u001b[1;33;40mNose Gear Not Up\u001b[0m"
|
||||||
|
],
|
||||||
|
"detail": "These AMBER CAS messages appear after selecting gear up with a positive rate of climb. The gear has failed to retract — follow the AFM checklist. Do not attempt to cycle the gear without consulting the checklist first."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 6,
|
||||||
|
"question": "If Tiller Steering Fails, what type of rudder pedal authority would you expect?",
|
||||||
|
"answer": "16° (AFM) / 17° (PAS)",
|
||||||
|
"ref": "AFM, Tiller Steering Fail; PAS / Landing Gear and Brakes",
|
||||||
|
"cas": "\u001b[1;33;40mTiller Steering Fail\u001b[0m",
|
||||||
|
"detail": "Normal tiller authority is 82°, and normal pedal steering is 7°. With tiller failure, the pedals get increased authority (16-17°) as a compensating mode. This is enough for taxi but tight turns will require more planning."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 7,
|
||||||
|
"question": "How does touchdown protection work?",
|
||||||
|
"answer": "Zero brake pressure is applied until:\n1. Wheel Speed > 70 kts\nOR\n2. WOW plus 5 seconds",
|
||||||
|
"ref": "PAS / Landing Gear and Brakes"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 8,
|
||||||
|
"question": "Is there a CAS message to identify the landing gear at the LGCMP is not in the NORM mode?",
|
||||||
|
"answer": "Yes: \u001b[1;33;40mLGCU Maintenance Mode\u001b[0m",
|
||||||
|
"ref": "AFM, LGCU Maintenance Mode",
|
||||||
|
"cas": "\u001b[1;33;40mLGCU Maintenance Mode\u001b[0m",
|
||||||
|
"detail": "After opening gear doors for preflight, you must close all gear doors AND select Normal on the LGCMP. If you forget, the plane stays in MX mode and the gear will not retract on departure. This CAS message is your safety net."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
1875
decks/g700-set01a-oral_study.json
Normal file
1875
decks/g700-set01a-oral_study.json
Normal file
File diff suppressed because it is too large
Load diff
287
decks/g700-set01b-flashcards.json
Normal file
287
decks/g700-set01b-flashcards.json
Normal file
|
|
@ -0,0 +1,287 @@
|
||||||
|
{
|
||||||
|
"title": "G700 Memory Flashcards",
|
||||||
|
"categories": [
|
||||||
|
{
|
||||||
|
"name": "Limitations",
|
||||||
|
"questions": [
|
||||||
|
{
|
||||||
|
"question": "Maximum slope approved for takeoff and landing?",
|
||||||
|
"answer": "+/- 2%"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"question": "Maximum crosswind component for takeoff:",
|
||||||
|
"answer": "30 knots"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"question": "Maximum tailwind component for takeoff:",
|
||||||
|
"answer": "10 knots"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"question": "Maximum turbulence penetration speed:",
|
||||||
|
"answer": "At or above 10,000 feet: 270 knots or .85 Mach whichever is less. Below 10,000 feet: 240 knots."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"question": "Maximum rain and hail penetration speed:",
|
||||||
|
"answer": "270 knots or .80 Mach"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"question": "Maximum operating speed:",
|
||||||
|
"answer": ".935 Mach"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"question": "Minimum distance to operate weather radar near fueling operations:",
|
||||||
|
"answer": "50 feet"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"question": "Minimum distance to operate weather radar near ground personnel:",
|
||||||
|
"answer": "11 feet"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"question": "Minimum height to engage the autopilot:",
|
||||||
|
"answer": "200 feet AGL"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"question": "Autopilot disengage height from ILS or LPV approach:",
|
||||||
|
"answer": "65 feet AGL"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"question": "Autopilot disengage height for other operations:",
|
||||||
|
"answer": "200 feet AGL"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"question": "Maximum demonstrated altitude loss for a coupled go-around:",
|
||||||
|
"answer": "60 feet"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"question": "Use of Custom Approach function:",
|
||||||
|
"answer": "Must only be used in visual meteorological conditions. Additionally, pilots are required to visually maintain a safe path to runway clear of obstacles."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"question": "Use of VOR Approach (VORAP) mode:",
|
||||||
|
"answer": "Prohibited if VOR station overflight is required during any portion of intermediate or final approach segments, excluding the missed approach point."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"question": "When should terrain be inhibited?",
|
||||||
|
"answer": "For airports not contained in EGPWF database."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Electrical",
|
||||||
|
"questions": [
|
||||||
|
{
|
||||||
|
"question": "Starting APU using external DC power source?",
|
||||||
|
"answer": "Prohibited"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Flight Controls",
|
||||||
|
"questions": [
|
||||||
|
{
|
||||||
|
"question": "Maximum flaps extended speed - VFE: Flaps 10",
|
||||||
|
"answer": "250 knots"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"question": "Maximum flaps extended speed - VFE: Flaps 20",
|
||||||
|
"answer": "220 knots"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"question": "Maximum flaps extended speed - VFE: Flaps DOWN",
|
||||||
|
"answer": "190 knots"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"question": "Maximum altitude for flaps 10 or 20:",
|
||||||
|
"answer": "FL250"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"question": "Maximum altitude for flaps DOWN",
|
||||||
|
"answer": "FL200"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"question": "Restrictions when operating in a mode other than Normal:",
|
||||||
|
"answer": "\n1. Max speed - 285 knots / .90 Mach\n2. Crosswind for landing - 10 knots\n3. Flight into known icing is prohibited"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"question": "Maneuvering speed: VA",
|
||||||
|
"answer": "206 knots"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"question": "Maximum speed if primary flight control surface (other than rudder) or spoiler panel is failed, caused by either a component malfunction or hydraulic failure:",
|
||||||
|
"answer": "285 knots / .90 Mach"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Landing Gear & Brakes",
|
||||||
|
"questions": [
|
||||||
|
{
|
||||||
|
"question": "Maximum altitude for landing gear extension / operation:",
|
||||||
|
"answer": "FL200"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"question": "Maximum landing gear operation speed: Normal operation - VLO",
|
||||||
|
"answer": "225 knots"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"question": "Maximum landing gear operation speed: Alternate extension - VLO",
|
||||||
|
"answer": "175 knots"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"question": "Maximum landing gear extended speed: VLE",
|
||||||
|
"answer": "250 knots"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"question": "Maximum nosewheel steering authority: Tiller",
|
||||||
|
"answer": "+/- 82 degrees"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"question": "Maximum nosewheel steering authority: Pedals",
|
||||||
|
"answer": "+/- 7 degrees"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Pneumatics / ECS",
|
||||||
|
"questions": [
|
||||||
|
{
|
||||||
|
"question": "Maximum cabin pressure differential permitted:",
|
||||||
|
"answer": "10.69 psi"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"question": "14 CFR 135.89 Supplemental Oxygen requirements above 35,000 feet:",
|
||||||
|
"answer": "One pilot must mask."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"question": "14 CFR 135.89 Supplemental Oxygen requirements above 25,000 feet:",
|
||||||
|
"answer": "One pilot must mask if other pilot leaves the cockpit."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "APU",
|
||||||
|
"questions": [
|
||||||
|
{
|
||||||
|
"question": "APU starting limits:",
|
||||||
|
"answer": "\nLimited to a maximum of three consecutive start attempts, with:\na 1 minute cool down period between attempts.\nAfter three start attempts, observe a 1 hour cool down period before subsequent starts are attempted."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"question": "APU guaranteed start altitude:",
|
||||||
|
"answer": "FL370"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"question": "APU maximum operating altitude:",
|
||||||
|
"answer": "FL450"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"question": "Maximum altitude for APU assisted engine start:",
|
||||||
|
"answer": "FL300"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Powerplant",
|
||||||
|
"questions": [
|
||||||
|
{
|
||||||
|
"question": "Engine starter duty cycle:",
|
||||||
|
"answer": "\n2 start cycles with a maximum of 3 minutes per start cycle, or\n3 start cycles with a maximum of 2 minutes per start cycle.\n\nDelay 15 seconds between start cycles.\nAfter 6 minutes, delay use of starter for at least 15 minutes."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"question": "Maximum crosswind component for engine start:",
|
||||||
|
"answer": "30 knots"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"question": "Maximum tailwind component for engine start:",
|
||||||
|
"answer": "20 knots"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"question": "Minimum oil temperature for engine ground start:",
|
||||||
|
"answer": "-35 C"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"question": "Maximum inflight start TGT:",
|
||||||
|
"answer": "850 C"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"question": "Time limitation for use of takeoff thrust:",
|
||||||
|
"answer": "\nSingle engine - 10 minutes,\nBoth engines - 5 minutes"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"question": "Maximum takeoff TGT:",
|
||||||
|
"answer": "950 C for 2 minutes"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"question": "Maximum continuous TGT:",
|
||||||
|
"answer": "940 C"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"question": "Speed for cancelling reverse thrust:",
|
||||||
|
"answer": "Idle by 60 knots"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"question": "Requirement if reverse thrust is used to bring the airplane to a halt in an emergency:",
|
||||||
|
"answer": "Record and report for maintenance action"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"question": "Use of thrust reversers for backing the airplane:",
|
||||||
|
"answer": "Prohibited"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Ice Protection",
|
||||||
|
"questions": [
|
||||||
|
{
|
||||||
|
"question": "Temperature at which CAI and WAI required for taxi and takeoff with visible moisture, precipitation, or wet runway present:",
|
||||||
|
"answer": "+10 C"
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"question": "When is takeoff prohibited?",
|
||||||
|
"answer": "When frost, ice, snow, or slush is adhering to critical surfaces"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"question": "When does anti-ice operate automatically?",
|
||||||
|
"answer": "Surface to FL350"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"question": "Use of wing and cowl anti-ice?",
|
||||||
|
"answer": "Select prior to entry into icing conditions"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"question": "Use of flaps in icing conditions:",
|
||||||
|
"answer": "Restricted to takeoff, approach and landing only.\n\nSelect WAI on and confirm operation in normal temperature range prior to landing gear or flap extension."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"question": "Holding in icing conditions:",
|
||||||
|
"answer": "Limited to flaps up and landing gear up only. Additionally, maintain a minimum speed of 200 knots"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"question": "Operation in forecast or reported severe icing:",
|
||||||
|
"answer": "\nProhibited\n\nVerify WAI and CAI operation and exit icing conditions as soon as possible."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Fuel",
|
||||||
|
"questions": [
|
||||||
|
{
|
||||||
|
"question": "Fuel pump operation:",
|
||||||
|
"answer": "\nAll operable boost pumps must be on for all phases of flight unless fuel balancing is in progress."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"question": "Fuel load balance:",
|
||||||
|
"answer": "Balance fuel load before the imbalance exceeds 1,000 pounds."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"question": "Maximum fuel imbalanace: Takeoff",
|
||||||
|
"answer": "1,000 pounds"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"question": "Maximum fuel imbalance: In Flight",
|
||||||
|
"answer": "2,000 pounds"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
27
decks/g700-set01c-limitations-INCOMPLETE.json
Normal file
27
decks/g700-set01c-limitations-INCOMPLETE.json
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
{
|
||||||
|
"title": "G700 Limitations *INCOMPLETE*",
|
||||||
|
"categories": [
|
||||||
|
{
|
||||||
|
"name": "Anti-Ice",
|
||||||
|
"questions": [
|
||||||
|
{
|
||||||
|
"question": "If required for takeoff, Wing Anti-Ice and/or Cowl Anti-Ice must be selected ON at least how many minutes prior to takeoff?",
|
||||||
|
"answer": "2 Minutes"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"question": "With a dual bleed system, what is the maximum altitude with Wing Anti-Ice On?",
|
||||||
|
"answer": "FL410"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"question": "With a single bleed system, what is the maximum altitude with Wing Anti-Ice On?",
|
||||||
|
"answer": "FL350"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"question": "With single Wing Anti-Ice system, what is the maximum altitude for operation?",
|
||||||
|
"answer": "FL350"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
}
|
||||||
166
decks/g700-set01d-abbreviations.json
Normal file
166
decks/g700-set01d-abbreviations.json
Normal file
|
|
@ -0,0 +1,166 @@
|
||||||
|
{
|
||||||
|
"title": "G700 Aviation Abbreviations",
|
||||||
|
"categories": [
|
||||||
|
{
|
||||||
|
"name": "Aircraft Systems",
|
||||||
|
"questions": [
|
||||||
|
{"question": "What does APU stand for?", "answer": "Auxiliary Power Unit"},
|
||||||
|
{"question": "What does ECS stand for?", "answer": "Environmental Control System"},
|
||||||
|
{"question": "What does FADEC stand for?", "answer": "Full Authority Digital Engine Control"},
|
||||||
|
{"question": "What does DCN stand for?", "answer": "Data Concentration Network"},
|
||||||
|
{"question": "What does SPDS stand for?", "answer": "Secondary Power Distribution System"},
|
||||||
|
{"question": "What does CPCS stand for?", "answer": "Cabin Pressure Control System"},
|
||||||
|
{"question": "What does CPCU stand for?", "answer": "Cabin Pressure Control Unit"},
|
||||||
|
{"question": "What does BAC stand for?", "answer": "Bleed Air Controller"},
|
||||||
|
{"question": "What does BTMS stand for?", "answer": "Brake Temperature Monitoring System"},
|
||||||
|
{"question": "What does LGCU stand for?", "answer": "Landing Gear Control Unit"},
|
||||||
|
{"question": "What does LGCMP stand for?", "answer": "Landing Gear Control and Maintenance Panel"},
|
||||||
|
{"question": "What does TROV stand for?", "answer": "Thrust Reverser Outflow Valve (also: Temperature Regulating Outflow Valve)"},
|
||||||
|
{"question": "What does NWS stand for?", "answer": "Nose Wheel Steering"},
|
||||||
|
{"question": "What does WAI stand for?", "answer": "Wing Anti-Ice"},
|
||||||
|
{"question": "What does PTU stand for?", "answer": "Power Transfer Unit"},
|
||||||
|
{"question": "What does EEC stand for?", "answer": "Electronic Engine Control"},
|
||||||
|
{"question": "What does PMA stand for?", "answer": "Permanent Magnet Alternator"},
|
||||||
|
{"question": "What does HSTS stand for?", "answer": "Horizontal Stabilizer Trim System"},
|
||||||
|
{"question": "What does ECU stand for (APU context)?", "answer": "Electronic Control Unit"},
|
||||||
|
{"question": "What does MED stand for?", "answer": "Main Entry Door"},
|
||||||
|
{"question": "What does WOW stand for?", "answer": "Weight On Wheels"},
|
||||||
|
{"question": "What does NUC stand for (EVS context)?", "answer": "Non-Uniformity Correction"},
|
||||||
|
{"question": "What does TSS stand for?", "answer": "Trim Speed System"},
|
||||||
|
{"question": "What does RTO stand for?", "answer": "Rejected Takeoff"},
|
||||||
|
{"question": "What does EDM stand for?", "answer": "Emergency Descent Mode"}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Electrical",
|
||||||
|
"questions": [
|
||||||
|
{"question": "What does GCU stand for?", "answer": "Generator Control Unit"},
|
||||||
|
{"question": "What does IDG stand for?", "answer": "Integrated Drive Generator"},
|
||||||
|
{"question": "What does TRU stand for?", "answer": "Transformer Rectifier Unit"},
|
||||||
|
{"question": "What does BPCU stand for?", "answer": "Bus Power Control Unit"},
|
||||||
|
{"question": "What does SSPC stand for?", "answer": "Solid State Power Controller"},
|
||||||
|
{"question": "What does RAT stand for?", "answer": "Ram Air Turbine"},
|
||||||
|
{"question": "What does EBHA stand for?", "answer": "Electric Backup Hydraulic Actuator"},
|
||||||
|
{"question": "What does MCE stand for?", "answer": "Motor Controls Electronics"},
|
||||||
|
{"question": "What does UPS stand for (electrical context)?", "answer": "Uninterruptible Power Supply"},
|
||||||
|
{"question": "What does EBATT stand for?", "answer": "Emergency Battery"},
|
||||||
|
{"question": "What does ESS stand for (electrical context)?", "answer": "Essential (as in Essential Bus)"},
|
||||||
|
{"question": "What does VDC stand for?", "answer": "Volts Direct Current"},
|
||||||
|
{"question": "What does AC stand for (electrical)?", "answer": "Alternating Current"},
|
||||||
|
{"question": "What does DC stand for (electrical)?", "answer": "Direct Current"},
|
||||||
|
{"question": "What does REU stand for?", "answer": "Remote Electronics Unit"}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Avionics & Navigation",
|
||||||
|
"questions": [
|
||||||
|
{"question": "What does FMS stand for?", "answer": "Flight Management System"},
|
||||||
|
{"question": "What does PFD stand for?", "answer": "Primary Flight Display"},
|
||||||
|
{"question": "What does SFD stand for?", "answer": "Standby Flight Display"},
|
||||||
|
{"question": "What does HUD stand for?", "answer": "Head-Up Display"},
|
||||||
|
{"question": "What does EVS stand for?", "answer": "Enhanced Vision System"},
|
||||||
|
{"question": "What does EFVS stand for?", "answer": "Enhanced Flight Vision System"},
|
||||||
|
{"question": "What does DU stand for?", "answer": "Display Unit"},
|
||||||
|
{"question": "What does TSC stand for?", "answer": "Touch Screen Controller"},
|
||||||
|
{"question": "What does OHPTS stand for?", "answer": "Overhead Panel Touch Screen"},
|
||||||
|
{"question": "What does CCD stand for?", "answer": "Cursor Control Device"},
|
||||||
|
{"question": "What does FCC stand for (flight controls)?", "answer": "Flight Control Computer"},
|
||||||
|
{"question": "What does BFCU stand for?", "answer": "Backup Flight Control Unit"},
|
||||||
|
{"question": "What does FCS stand for?", "answer": "Flight Control System"},
|
||||||
|
{"question": "What does IRS stand for?", "answer": "Inertial Reference System"},
|
||||||
|
{"question": "What does AHRS stand for?", "answer": "Attitude and Heading Reference System"},
|
||||||
|
{"question": "What does ADS stand for?", "answer": "Air Data System"},
|
||||||
|
{"question": "What does MAU stand for?", "answer": "Modular Avionics Unit"},
|
||||||
|
{"question": "What does VOR stand for?", "answer": "VHF Omnidirectional Range"},
|
||||||
|
{"question": "What does ILS stand for?", "answer": "Instrument Landing System"},
|
||||||
|
{"question": "What does RNAV stand for?", "answer": "Area Navigation"},
|
||||||
|
{"question": "What does LPV stand for?", "answer": "Localizer Performance with Vertical guidance"},
|
||||||
|
{"question": "What does LNAV stand for?", "answer": "Lateral Navigation"},
|
||||||
|
{"question": "What does GLS stand for?", "answer": "GBAS Landing System"},
|
||||||
|
{"question": "What does GBAS stand for?", "answer": "Ground-Based Augmentation System"},
|
||||||
|
{"question": "What does WAAS stand for?", "answer": "Wide Area Augmentation System"},
|
||||||
|
{"question": "What does GPS stand for?", "answer": "Global Positioning System"},
|
||||||
|
{"question": "What does EGPWF stand for?", "answer": "Enhanced Ground Proximity Warning Function"},
|
||||||
|
{"question": "What does GPWS stand for?", "answer": "Ground Proximity Warning System"},
|
||||||
|
{"question": "What does CVR stand for?", "answer": "Cockpit Voice Recorder"},
|
||||||
|
{"question": "What does ELT stand for?", "answer": "Emergency Locator Transmitter"},
|
||||||
|
{"question": "What does IR stand for (EVS context)?", "answer": "Infrared"},
|
||||||
|
{"question": "What does VORAP stand for?", "answer": "VOR Approach (mode)"},
|
||||||
|
{"question": "What does VSD stand for?", "answer": "Vertical Situation Display"},
|
||||||
|
{"question": "What does FD stand for?", "answer": "Flight Director"},
|
||||||
|
{"question": "What does AP stand for?", "answer": "Autopilot"},
|
||||||
|
{"question": "What does CAS stand for?", "answer": "Crew Alerting System"},
|
||||||
|
{"question": "What does POF stand for?", "answer": "Pilot Options Function"},
|
||||||
|
{"question": "What does TOGA stand for?", "answer": "Takeoff / Go-Around"}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Performance & Speeds",
|
||||||
|
"questions": [
|
||||||
|
{"question": "What does KCAS stand for?", "answer": "Knots Calibrated Airspeed"},
|
||||||
|
{"question": "What does KIAS stand for?", "answer": "Knots Indicated Airspeed"},
|
||||||
|
{"question": "What does KTAS stand for?", "answer": "Knots True Airspeed"},
|
||||||
|
{"question": "What does VMO stand for?", "answer": "Maximum Operating Speed (Velocity Max Operating)"},
|
||||||
|
{"question": "What does MMO stand for?", "answer": "Maximum Operating Mach Number"},
|
||||||
|
{"question": "What does VREF stand for?", "answer": "Reference Landing Speed"},
|
||||||
|
{"question": "What does AOA stand for?", "answer": "Angle of Attack"},
|
||||||
|
{"question": "What does FL stand for?", "answer": "Flight Level"},
|
||||||
|
{"question": "What does AGL stand for?", "answer": "Above Ground Level"},
|
||||||
|
{"question": "What does MSL stand for?", "answer": "Mean Sea Level"},
|
||||||
|
{"question": "What does HAT stand for?", "answer": "Height Above Touchdown"},
|
||||||
|
{"question": "What does RA stand for?", "answer": "Radio Altimeter (or Radio Altitude)"},
|
||||||
|
{"question": "What does DA stand for?", "answer": "Decision Altitude"},
|
||||||
|
{"question": "What does DH stand for?", "answer": "Decision Height"},
|
||||||
|
{"question": "What does MDA stand for?", "answer": "Minimum Descent Altitude"},
|
||||||
|
{"question": "What does RVR stand for?", "answer": "Runway Visual Range"},
|
||||||
|
{"question": "What does TGT stand for?", "answer": "Turbine Gas Temperature"},
|
||||||
|
{"question": "What does EGT stand for?", "answer": "Exhaust Gas Temperature"},
|
||||||
|
{"question": "What does LP stand for (engine)?", "answer": "Low Pressure (spool/shaft)"},
|
||||||
|
{"question": "What does HP stand for (engine)?", "answer": "High Pressure (spool/shaft)"},
|
||||||
|
{"question": "What does PSI stand for?", "answer": "Pounds per Square Inch"}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Documents & References",
|
||||||
|
"questions": [
|
||||||
|
{"question": "What does AFM stand for?", "answer": "Airplane Flight Manual"},
|
||||||
|
{"question": "What does OM stand for?", "answer": "Operations Manual"},
|
||||||
|
{"question": "What does PAS stand for?", "answer": "Pilot Awareness Study (guide)"},
|
||||||
|
{"question": "What does PTM stand for?", "answer": "Pilot Training Manual"},
|
||||||
|
{"question": "What does MEL stand for?", "answer": "Minimum Equipment List"},
|
||||||
|
{"question": "What does FAR stand for?", "answer": "Federal Aviation Regulation"},
|
||||||
|
{"question": "What does CFR stand for?", "answer": "Code of Federal Regulations"},
|
||||||
|
{"question": "What does ACS stand for?", "answer": "Airman Certification Standards"},
|
||||||
|
{"question": "What does ATP stand for?", "answer": "Airline Transport Pilot"},
|
||||||
|
{"question": "What does ICAO stand for?", "answer": "International Civil Aviation Organization"},
|
||||||
|
{"question": "What does FAA stand for?", "answer": "Federal Aviation Administration"},
|
||||||
|
{"question": "What does FSB stand for?", "answer": "Flight Standardization Board"},
|
||||||
|
{"question": "What does SM stand for (visibility)?", "answer": "Statute Mile"}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "FMS / Interface Terms",
|
||||||
|
"questions": [
|
||||||
|
{"question": "What does FPLN stand for (TSC context)?", "answer": "Flight Plan"},
|
||||||
|
{"question": "What does PERF stand for (FMS context)?", "answer": "Performance"},
|
||||||
|
{"question": "What does INIT stand for (FMS context)?", "answer": "Initialization"},
|
||||||
|
{"question": "What does DEP stand for (FMS context)?", "answer": "Departure"},
|
||||||
|
{"question": "What does APR stand for (FMS context)?", "answer": "Approach"},
|
||||||
|
{"question": "What does WX stand for?", "answer": "Weather"},
|
||||||
|
{"question": "What does MET stand for?", "answer": "Meteorological"},
|
||||||
|
{"question": "What does HDG stand for?", "answer": "Heading"},
|
||||||
|
{"question": "What does ALT stand for?", "answer": "Altitude (or Alternate)"},
|
||||||
|
{"question": "What does CLB stand for?", "answer": "Climb"},
|
||||||
|
{"question": "What does CRZ stand for?", "answer": "Cruise"},
|
||||||
|
{"question": "What does NAV stand for?", "answer": "Navigation"},
|
||||||
|
{"question": "What does DISC stand for (AP context)?", "answer": "Disconnect"},
|
||||||
|
{"question": "What does GND stand for?", "answer": "Ground"},
|
||||||
|
{"question": "What does SVC stand for?", "answer": "Service"},
|
||||||
|
{"question": "What does BARO stand for?", "answer": "Barometric"},
|
||||||
|
{"question": "What does REV stand for (engine display)?", "answer": "Reverse (thrust reverser indication)"},
|
||||||
|
{"question": "What does BIT stand for?", "answer": "Built-In Test"},
|
||||||
|
{"question": "What does MX stand for?", "answer": "Maintenance"}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
1868
decks/g700-set01e-ground_study_guide.json
Normal file
1868
decks/g700-set01e-ground_study_guide.json
Normal file
File diff suppressed because it is too large
Load diff
4
docs/ANSI Color Escapes.txt
Normal file
4
docs/ANSI Color Escapes.txt
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
\u001b[1;37;40mWHITE TEXT\u001b[0m
|
||||||
|
\u001b[1;36;40mCYAN TEXT\u001b[0m
|
||||||
|
\u001b[1;31;40mRED TEXT\u001b[0m
|
||||||
|
\u001b[1;33;40mAMBER TEXT\u001b[0m
|
||||||
347
docs/DEVDOC.md
Normal file
347
docs/DEVDOC.md
Normal file
|
|
@ -0,0 +1,347 @@
|
||||||
|
# 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.
|
||||||
135
docs/README.md
Normal file
135
docs/README.md
Normal file
|
|
@ -0,0 +1,135 @@
|
||||||
|
# AWARE — Aviation Weighted Active Recall Engine
|
||||||
|
|
||||||
|
A cross-platform CLI study tool built for aviation knowledge retention.
|
||||||
|
Named after the AWARE briefing — the same commitment to situational
|
||||||
|
awareness in the cockpit, applied to systems knowledge on the ground.
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd aware
|
||||||
|
python3 aware.py
|
||||||
|
```
|
||||||
|
|
||||||
|
## Text-to-Speech
|
||||||
|
|
||||||
|
Voice is auto-detected by platform — no configuration needed:
|
||||||
|
|
||||||
|
| Platform | Engine | Install |
|
||||||
|
|----------|--------|---------|
|
||||||
|
| Linux | espeak-ng | `sudo apt install espeak-ng` |
|
||||||
|
| macOS | say | Built-in, nothing to install |
|
||||||
|
| Windows | SAPI (PowerShell) | Built-in, nothing to install |
|
||||||
|
|
||||||
|
Toggle voice on/off with `v` from any menu or during a quiz.
|
||||||
|
|
||||||
|
### Pronunciation Glossary
|
||||||
|
|
||||||
|
The file `tts_glossary.json` (next to `aware.py`) controls how abbreviations are spoken.
|
||||||
|
Edit it to add, remove, or change pronunciations:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"abbreviations": {
|
||||||
|
"APU": "A-P-U",
|
||||||
|
"DU": "display unit",
|
||||||
|
"FADEC": "FADEC"
|
||||||
|
},
|
||||||
|
"symbols": {
|
||||||
|
">=": "greater than or equal to",
|
||||||
|
"\u00b0": " degrees "
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Abbreviations are matched as **whole words only** — "DU" won't corrupt "DURING" or "PROCEDURE."
|
||||||
|
Longer abbreviations match first, so "KCAS" is caught before "CAS" can interfere.
|
||||||
|
|
||||||
|
### Custom TTS Override
|
||||||
|
|
||||||
|
To use a different engine (e.g. `piper` for better voice quality),
|
||||||
|
create `~/.aware/config.json`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"tts_command": "piper",
|
||||||
|
"tts_args": ["--model", "en_US-lessac-medium", "--output-raw", "{text}"]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Use `{text}` as the placeholder — it gets replaced with the spoken text.
|
||||||
|
|
||||||
|
## Adding New Decks
|
||||||
|
|
||||||
|
Drop any `.json` file into the `decks/` folder. Two formats supported:
|
||||||
|
|
||||||
|
### Minimal (flat)
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"title": "My Study Deck",
|
||||||
|
"questions": [
|
||||||
|
{"question": "What is X?", "answer": "X is Y."}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Full (with categories and references)
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"title": "My Study Deck",
|
||||||
|
"categories": [
|
||||||
|
{
|
||||||
|
"name": "Topic A",
|
||||||
|
"questions": [
|
||||||
|
{"id": 1, "question": "...", "answer": "...", "ref": "Chapter 3"}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- `id` is auto-assigned if omitted
|
||||||
|
- `ref` is optional (displays after the answer)
|
||||||
|
- `category` enables the "By Category" filter in the menu
|
||||||
|
- `cas` is optional (string or array — displays CAS messages above the question)
|
||||||
|
- `detail` is optional (extended explanation displayed after the answer)
|
||||||
|
|
||||||
|
## Controls
|
||||||
|
|
||||||
|
After revealing an answer, grade yourself:
|
||||||
|
|
||||||
|
- **n** — Didn't get the answer at all → Bucket 1
|
||||||
|
- **1** — Got some of the answer → Bucket 2
|
||||||
|
- **2** — Got about half right → Bucket 3
|
||||||
|
- **3** — Got most of the answer → Bucket 4
|
||||||
|
- **y** — Nailed it → promotes by one (max Bucket 5)
|
||||||
|
|
||||||
|
Numpad aliases: **-** = n, **+** = y. Bare **Enter** defaults to missed.
|
||||||
|
|
||||||
|
Other keys:
|
||||||
|
- **v** — Toggle text-to-speech on/off
|
||||||
|
- **q** — Quit current session (progress is saved)
|
||||||
|
- **d** — Switch to a different deck
|
||||||
|
|
||||||
|
## Weighted Repetition
|
||||||
|
|
||||||
|
Questions live in 5 buckets:
|
||||||
|
- **Bucket 1** (No Clue) — 8x selection weight
|
||||||
|
- **Bucket 2** (Some) — 5x weight
|
||||||
|
- **Bucket 3** (Half) — 3x weight
|
||||||
|
- **Bucket 4** (Most) — 2x weight
|
||||||
|
- **Bucket 5** (Nailed It) — 1x weight
|
||||||
|
|
||||||
|
Grades `n` through `3` set the bucket directly based on self-assessment.
|
||||||
|
Grade `y` promotes by one — meaning you have to nail it from Bucket 4 to reach
|
||||||
|
Bucket 5, which matches the physical flashcard pattern of "getting it right regularly."
|
||||||
|
|
||||||
|
## Progress
|
||||||
|
|
||||||
|
Per-deck progress saved to `~/.aware/progress/`.
|
||||||
|
Reset per-deck via menu option 7, or delete the file to reset:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
rm ~/.aware/progress/deckname_*.json # single deck
|
||||||
|
rm -rf ~/.aware/progress/ # all decks
|
||||||
|
```
|
||||||
105
tts_glossary.json
Normal file
105
tts_glossary.json
Normal file
|
|
@ -0,0 +1,105 @@
|
||||||
|
{
|
||||||
|
"_comment": "TTS pronunciation glossary. Keys are matched as whole words (case-sensitive). Values are what gets spoken. Edit freely — AWARE loads this at startup.",
|
||||||
|
"abbreviations": {
|
||||||
|
"AC": "A-C",
|
||||||
|
"ADS": "air data system",
|
||||||
|
"AFM": "A-F-M",
|
||||||
|
"AGL": "A-G-L",
|
||||||
|
"AOA": "angle of attack",
|
||||||
|
"AP": "autopilot",
|
||||||
|
"APU": "A-P-U",
|
||||||
|
"BACs": "bleed air controllers",
|
||||||
|
"BFCU": "backup flight control unit",
|
||||||
|
"BPCU": "B-P-C-U",
|
||||||
|
"BTMS": "brake temperature monitoring system",
|
||||||
|
"CAS": "C-A-S",
|
||||||
|
"CPCS": "C-P-C-S",
|
||||||
|
"CPCU": "C-P-C-U",
|
||||||
|
"CVR": "cockpit voice recorder",
|
||||||
|
"DA": "decision altitude",
|
||||||
|
"DC": "D-C",
|
||||||
|
"DCN": "D-C-N",
|
||||||
|
"DH": "decision height",
|
||||||
|
"DU": "display unit",
|
||||||
|
"EBHA": "E-B-H-A",
|
||||||
|
"ECS": "E-C-S",
|
||||||
|
"EDM": "emergency descent mode",
|
||||||
|
"EEC": "E-E-C",
|
||||||
|
"EECs": "E-E-Cs",
|
||||||
|
"EFVS": "enhanced flight vision system",
|
||||||
|
"EGPWF": "E-G-P-W-F",
|
||||||
|
"EGT": "E-G-T",
|
||||||
|
"ELT": "E-L-T",
|
||||||
|
"EVS": "enhanced vision system",
|
||||||
|
"FADEC": "FADEC",
|
||||||
|
"FCC": "F-C-C",
|
||||||
|
"FCCs": "F-C-Cs",
|
||||||
|
"FCS": "F-C-S",
|
||||||
|
"FD": "flight director",
|
||||||
|
"FL": "flight level",
|
||||||
|
"FMS": "F-M-S",
|
||||||
|
"GCU": "G-C-U",
|
||||||
|
"HAT": "H-A-T",
|
||||||
|
"HP": "H-P",
|
||||||
|
"HSTS": "H-S-T-S",
|
||||||
|
"HUD": "heads up display",
|
||||||
|
"IDG": "I-D-G",
|
||||||
|
"IR": "infrared",
|
||||||
|
"IRS": "I-R-S",
|
||||||
|
"KCAS": "K-CAS",
|
||||||
|
"LGCMP": "landing gear control maintenance panel",
|
||||||
|
"LGCU": "L-G-C-U",
|
||||||
|
"LP": "L-P",
|
||||||
|
"LPV": "L-P-V",
|
||||||
|
"MAU": "M-A-U",
|
||||||
|
"MCE": "M-C-E",
|
||||||
|
"MDA": "minimum descent altitude",
|
||||||
|
"MED": "main entry door",
|
||||||
|
"MEL": "M-E-L",
|
||||||
|
"MMO": "M-M-O",
|
||||||
|
"NUC": "non-uniformity correction",
|
||||||
|
"NWS": "nose wheel steering",
|
||||||
|
"OHPTS": "overhead panel touchscreen",
|
||||||
|
"PAS": "P-A-S",
|
||||||
|
"PFD": "primary flight display",
|
||||||
|
"PMA": "P-M-A",
|
||||||
|
"POF": "P-O-F",
|
||||||
|
"PSI": "P-S-I",
|
||||||
|
"PTM": "P-T-M",
|
||||||
|
"PTU": "P-T-U",
|
||||||
|
"RA": "radio altimeter",
|
||||||
|
"RAT": "ram air turbine",
|
||||||
|
"REU": "R-E-U",
|
||||||
|
"REUs": "R-E-Us",
|
||||||
|
"RNAV": "R-NAV",
|
||||||
|
"RTO": "R-T-O",
|
||||||
|
"RVR": "R-V-R",
|
||||||
|
"SAPI": "S-A-P-I",
|
||||||
|
"SFD": "standby flight display",
|
||||||
|
"SPDS": "S-P-D-S",
|
||||||
|
"SSPC": "S-S-P-C",
|
||||||
|
"TGT": "T-G-T",
|
||||||
|
"TOGA": "toe-ga",
|
||||||
|
"TROV": "T-R-O-V",
|
||||||
|
"TRU": "T-R-U",
|
||||||
|
"TRUs": "T-R-Us",
|
||||||
|
"TSC": "T-S-C",
|
||||||
|
"TSS": "trim speed system",
|
||||||
|
"UPS": "U-P-S",
|
||||||
|
"VMO": "V-M-O",
|
||||||
|
"VOR": "V-O-R",
|
||||||
|
"VORAP": "V-O-R-A-P",
|
||||||
|
"VSD": "V-S-D",
|
||||||
|
"WAI": "wing anti-ice",
|
||||||
|
"WOW": "weight on wheels",
|
||||||
|
"WSHLD": "windshield"
|
||||||
|
},
|
||||||
|
"symbols": {
|
||||||
|
">=": "greater than or equal to",
|
||||||
|
"<=": "less than or equal to",
|
||||||
|
">": "greater than",
|
||||||
|
"<": "less than",
|
||||||
|
"\u00b0": " degrees ",
|
||||||
|
"\n": ". "
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue