AutoNote is a desktop application that generates comprehensive study notes from Canvas LMS lecture materials and videos. It uses an Electron frontend, a set of Python pipeline scripts executed as subprocesses, and an isolated ML virtual environment for GPU-accelerated processing.
User Interface (Electron)
│
├── main.js IPC bridge, subprocess management, file operations
├── renderer/app.js Single-page app (7 pages), terminal, state management
└── preload.js Secure IPC bridge between renderer and main process
│
▼
Pipeline Scripts (Python, executed as subprocesses)
│
├── downloader.py Canvas + Panopto download
├── extract_caption.py Whisper transcription
├── frame_extractor.py Screen-share frame extraction + dedup + junk filter
├── semantic_alignment.py Transcript ↔ slide alignment (FAISS + Viterbi)
├── alignment_parser.py Compact alignment JSON for LLM prompts
├── pipeline_worker.py Orchestrates transcribe + frame-extract + align
├── note_generation.py LLM-based note writing + image filtering
└── benchmark.py Quality evaluation (coverage / image / coherency)
│
▼
ML Environment (~/.auto_note/venv/)
PyTorch, faster-whisper, sentence-transformers, FAISS, BGE-M3
Canvas API Panopto API
│ │
▼ ▼
materials/ (PDF, PPTX) videos/ (MP4)
│ │
│ ┌─────┴──────┐
│ │ │
│ Camera? Screen share?
│ │ │
│ ▼ ▼
│ captions/ frames/<stem>/
│ (Whisper) (ffmpeg scene detect
│ + periodic sampling
│ + perceptual hash dedup
│ + junk filter via vision API)
│ + image_cache.json (descriptions)
│ │ │
└────────┬───────────┘ │
│ │
▼ ▼
alignment/ alignment/
(BGE-M3 matching → (timestamp-based
FAISS + Viterbi) frame assignment)
│ │
└────────┬───────────────┘
│
▼
alignment/*.compact.json
(10x smaller for LLM prompts)
│
▼
notes/sections/L{N}_S{ci}.md
(per-chunk LLM generation, cached)
│
▼
notes/CourseName_notes.md
(merged + image-filtered final output)
Architecture: Single-page application with 7 pages and a persistent terminal panel.
State management: Global State object in app.js persists form values across page navigation. Key fields include pipeline.courseId, pipeline.language, pipeline.detail, pipeline.force, and step checkboxes.
Subprocess execution: main.js spawns Python scripts via child_process.spawn() (or node-pty when available for tty support). Stdout/stderr are streamed to the renderer via IPC events (process:data, process:done).
Dashboard detail modal: Clicking a course card opens a modal overlay that lists all transcribed videos with their processing status (caption/alignment/notes). Each video has a delete button that removes the transcript, alignment, note sections, and per-video note files via the course:deleteVideo IPC handler.
Packaging (electron/package.json):
- Windows: NSIS installer only (
target: ["nsis"]). Theappxtarget was removed — it requires Microsoft Store signing credentials that aren't available in CI. - macOS: Universal DMG (x64 + arm64) with signing explicitly disabled (
identity: null,hardenedRuntime: false,gatekeeperAssess: false,dmg.sign: false). Without these, users see a "AutoNote.app is damaged and can't be opened" quarantine error. Users still need to right-click → Open on first launch (standard for unsigned apps). - Linux: AppImage + deb.
extraResourcesmust list every Python script referenced bymain.js:SCRIPTSor the renderer. Current list:downloader.py,extract_caption.py,frame_extractor.py,semantic_alignment.py,alignment_parser.py,note_generation.py,pipeline_worker.py. Missing a script from this list leaves it out ofresources/scripts/in the packaged app and causes "No such file or directory" errors on users' machines.
- Downloads videos from Panopto and materials from Canvas
- Stream priority: OBJECT > SS > DV > untagged. OBJECT streams are screen recordings and preferred over camera (DV) when both are available for a lecture.
- Tracks download state in
manifest.jsonanddownload_log.json. Each entry recordsstream_tag(used downstream byframe_extractor). - Slack mode adds random delays to avoid rate-limiting
- Smart size filter uses LLM to select relevant files when > 1 GB
- Selects backend automatically: faster-whisper (GPU) or OpenAI Whisper API
- Produces timestamped segment-level JSON
--forceflag re-transcribes even if captions already exist- Language detection probes from mid-audio for accuracy
- Whisper hallucination filter: segments whose text is only
. . . . .(common during silent audio from OBJECT-stream intro/loading screens) are dropped downstream inalignment_parser._clean_transcript.
- Classifies videos as screen-share or camera using edge/uniformity heuristics. Screen recordings have sharp edges, high brightness, and large uniform regions.
- Per-stream extraction tuning (
detect_scenes(stream_tag=...)):- For
OBJECT/SSstreams (stable slide recordings with subtle text-only changes): scene threshold lowered to 0.15 and periodic samples taken every 30s unconditionally. Without periodic sampling, 2-hour OBJECT streams yield only ~10 frames. - Other streams: default 0.3 threshold, periodic sampling only as fallback when < 5 scene changes detected.
- For
- Same-page deduplication: Groups consecutive frames by perceptual hash similarity (dHash, 16×16 = 256-bit hash, Hamming distance <
PAGE_SIMILARITY_THRESHOLD = 35bits). Merges incremental bullet reveals while keeping genuinely different pages. From each group, selects the frame with the highest visual information score (edge density on 160x120 grayscale). - Junk-frame filter (
_JUNK_DESC_RE+_is_blank_frame):- Pre-vision: pure-black or pure-white frames (>95% of samples <15 or >240 in grayscale) are skipped before calling the vision API.
- Post-vision: frames whose vision description matches
_JUNK_DESC_RE(desktop wallpaper, taskbar, Windows 11, loading screens, vision-API refusals, memes, XKCD, four-panel comics) are deleted. Remaining frames are contiguously renumbered on disk and in the alignment so they stay in sync.
- Vision API descriptions: After extraction,
_describe_framescalls GPT-4o-mini (viasemantic_alignment.ImageDescriber) to describe each frame. Descriptions are cached inframes/<stem>/image_cache.jsonkeyed bypage_N(0-indexed matching frame_{N+1:03d}.png). - Builds timestamp-based alignment JSON compatible with the rest of the pipeline. The
sourcefield is"screenshare"for frame-based alignments and"slides"for PDF-based ones.
- Single subprocess that orchestrates
extract_caption.py+frame_extractor.py+semantic_alignment.pyfor every video in a course. - Invoked by the Electron app (
main.js:SCRIPTS.pipeline_worker) for the "Transcribe + Align" button, letting one progress bar cover the whole pipeline. _script(name)resolves script paths by looking in~/.auto_note/scripts/first (production install), then the script's own directory (development). This matches how the Electron app syncs packagedresources/scripts/*.pyinto~/.auto_note/scripts/on first launch.- Must be listed in
electron/package.json:build.extraResourcesor the packaged Windows/macOS installer will be missing it.
- Extracts text from slides (PDF/PPTX/DOCX) with image enrichment for sparse slides
- Matching priority (in
process_course):- User-supplied mapping JSON
- Automatic name/number matching
- BGE-M3 embedding match (pre-computed via
suggest_matches) - mpnet content embedding fallback
- Embeds slides and transcript windows with sentence-transformers
- FAISS IndexFlatIP for fast cosine-similarity K-NN lookup
- Viterbi temporal smoothing with forward bias and temporal position prior
- Off-slide detection for Q&A/demo segments (cosine < threshold)
--forceflag re-aligns even if alignment files already existImageDescriberis reused byframe_extractorto describe screen-capture frames.
- Compresses full alignment JSON (300 KB) into compact per-slide format (30 KB)
_clean_transcriptstrips Whisper's dot-hallucination segments (re.fullmatch(r"[\s.]+", text)) and common ASR filler words.- Used by note_generation to build token-efficient LLM prompts
- Multi-provider LLM support: OpenAI, Anthropic, Google Gemini, DeepSeek, xAI, Mistral, Claude CLI (
claude -p) - Language system:
--language en|zhCLI flag overrides theNOTE_LANGUAGEconstant._P(key)always returns English prompts; Chinese output is produced by a separate post-generation_translate()call per section. - Translation prompt preserves technical terms in English when target is Chinese. The prompt lists protected term categories (protocols, crypto, algorithms, proper nouns, code identifiers) and instructs the LLM to translate only connecting prose. Example output:
symmetric key cryptography 方案在 encryption 和 decryption 时使用同一个 key。This matches how students study in English-taught courses — the Chinese provides narrative, the English terms remain exam-ready. - Per-lecture chunking: CHAPTER_SIZE slides per LLM call.
MAX_NOTE_CHARS = 120000is the total prompt-char cap (transcript + slide outlines + image hints) — replaces the older per-slideMAX_TRANSCRIPT_CHARSlimit that was truncating content too aggressively. - Parallel section generation:
ThreadPoolExecutor(max_workers=PARALLEL_SECTIONS = 6)fans out chunk generation + translation concurrently. Each chunk returns(ci, (content, fresh))— make sure to unpack asci_ret, (content, fresh) = fut.result(). - Section caching: each chunk saved as
L{N}_S{ci}.md.notes/sections/.languagemarker triggers auto-force-regen when the language changes. - Image hints for screen-share lectures (
make_chunk_prompt, line 819): now include the cached vision description (img_cache["page_N"]) instead of only the transcript context. Without this the LLM invents captions for frames based on what it's writing about, causing captions that don't match the displayed image. - Image rendering (
LectureData.render_chunk_images, line 1222): copies source frames fromframes/<stem>/frame_NNN.pngintonotes/images/L{NN}/. Refreshes stale copies when source mtime or size differs from the destination — important after re-extraction, or the displayed image will be an outdated frame with different content than the caption describes. --forceflag re-generates all sections from scratch- Image filtering: multi-step decision pipeline (cache description keywords → title pattern → vision API). Includes all slides with visual elements; only excludes administrative/non-course elements.
- Self-scoring: coverage, terminology, callouts, code blocks (weighted average)
- Per-video mode (
--per-video): one note file per lecture instead of merged - Iterative mode: raises detail level until quality target is reached
- All terminal output (print/tqdm.write) is in English regardless of note language
- Stand-alone quality evaluator for generated notes. Usage:
python benchmark.py --course ID [--verbose]or--note PATH --transcript PATH --image-cache PATH. - Three metrics, each scored 0-10:
- Content coverage (
content_coverage): extracts key terms (capitalized phrases, acronyms, hyphenated terms appearing ≥2 times) from the transcript; scores the % of those terms that appear in the note. - Image density (
image_density): ratio of images inserted in the note vs. number of content-rich images available inimage_cache.json(filtered via_CONTENT_KEYWORDSto exclude loading-screen/desktop/blank descriptions). - Logic coherency (
logic_coherency): structural checks — sections headings, long image clusters (≥3 consecutive images without intervening prose; pairs are allowed), orphan images (no preceding paragraph within 6 lines), mid-sentence truncation, leaked artifacts (APPROVED, © CS, LLM refusals).
- Content coverage (
- Overall =
0.4 × coverage + 0.3 × image_density + 0.3 × coherency. - Limitations: coverage extractor matches English terms only, so Chinese notes score lower on coverage even when content is complete. Image density returns 10.0 when no cache exists (division-by-zero fallback for slide-based notes).
The "Force regenerate" toggle applies to whichever pipeline steps are selected:
| Step selected | Without force | With force |
|---|---|---|
| Transcribe | Skips videos with existing captions | Re-transcribes all videos |
| Align | Skips captions with existing alignment | Re-aligns all captions |
| Generate notes | Uses cached section .md files | Re-calls LLM for all sections |
Without force, the pipeline is incremental: only missing files are processed.
Images pass through multiple filtering layers before appearing in the final notes:
- Frame extraction (
frame_extractor.py):_is_blank_framepre-vision skip for all-black / all-white frames_JUNK_DESC_REpost-vision filter for desktop wallpaper, loading screens, memes, XKCD, vision-API refusals
- Image hints generation: Slides with word_count < 80, cached descriptions, or code are offered to the LLM as available images. Screen-share frames always pass through their
image_cachedescription so the LLM caption matches the actual frame. - LLM prompt instructions: System prompt instructs to insert all slides with visual elements (diagrams, charts, code, math, etc.) and skip pure text or administrative slides
- Post-generation filter (
filter_images_pass):- Screen-share frames: always kept
- Cache-verified visual description: kept
- Title/divider pattern: removed
- Vision API (GPT-4o-mini): decides uncertain cases; defaults to keep
- Image rendering (
render_chunk_images): always refreshes stale copies innotes/images/L{NN}/on mtime/size mismatch so the final.mdnever points at stale content.
electron/build/uninstaller.nsh lets users choose what to keep on uninstall via three sequential MessageBox prompts, all inside the customUnInstall macro:
- Keep generated notes and downloaded course files in
%USERPROFILE%\AutoNote? (default: No) - Keep ML environment (~2 GB) in
%USERPROFILE%\.auto_note\venv? (default: No) - Keep settings and API keys in
%USERPROFILE%\.auto_note? (default: No)
Each MessageBox uses the NSIS label-jump idiom: IDYES keep_label → skip the following delete commands and jump past them. This keeps the implementation free of Var declarations, ${If}/${EndIf} macros, and LogicLib dependencies — all of which turned out to break electron-builder's multi-pass NSIS compile.
Three pragmas suppress warnings that fire harmlessly in one of the two compile passes but would be escalated to errors by CI:
!pragma warning disable 6010— "un.* function not referenced"!pragma warning disable 6020— "uninstaller script code but no WriteUninstaller"!pragma warning disable 8000— "Uninstall page instfiles not used"
Things that did not work during iteration (kept here to save future attempts):
- Custom
UninstPagevia!macro customUnInstallPage: that macro name is not a real electron-builder hook, so theun.function is never referenced and NSIS zeros it out with warning 6010. UninstPage custom ...inside!macro customHeader: valid NSIS location but triggers warning 8000 because it overrides MUI2's defaultMUI_UNPAGE_INSTFILESwithout a replacement.customUnInit+Var+${If}: compiled but failed in one of electron-builder's passes (exact error unavailable without admin log access). Moving the prompts intocustomUnInstalldirectly succeeded.
No database — all data is file-based JSON/Markdown.
| File | Location | Purpose |
|---|---|---|
config.json |
~/.auto_note/ |
Canvas URL, Panopto host, output dir |
*_api.txt / *_token.txt |
~/.auto_note/ |
API keys and tokens |
manifest.json |
~/.auto_note/ |
Video download state + stream_tag |
download_log.json |
Per-course | Material download tracking |
captions/*.json |
Per-course | Whisper transcript (timestamped segments) |
frames/<stem>/frame_NNN.png |
Per-course | Extracted screen-share frames |
frames/<stem>/image_cache.json |
Per-course | Vision-API descriptions per frame |
alignment/*.json |
Per-course | Full segment-level alignment |
alignment/*.compact.json |
Per-course | Token-efficient alignment for LLM |
notes/sections/L*_S*.md |
Per-course | Cached per-chunk note sections |
notes/sections/.language |
Per-course | Current language marker (forces regen on change) |
notes/*_notes.md |
Per-course | Final merged/per-video notes (English default) |
notes/*_notes.zh.md |
Per-course | Chinese version (user renames after --language zh) |
notes/*_notes.en.md |
Per-course | Optional English backup before running ZH regen |
notes/*.score.json |
Per-course | Self-score breakdown |
notes/images/L*/ |
Per-course | Rendered slide PNGs / copied frames |
Tests are in test/ and organized by scope:
| File | Scope |
|---|---|
test_unit.py |
Offline unit tests: Viterbi, timeline, hashing, alignment parsing, slide discovery |
test_pipeline.py |
Integration tests: script execution, manifest schema, CLI flags (some require network) |
test_language_and_skip.py |
Language selection, terminal output (no CJK in prints), skip logic with/without --force |
test_note_generation.py |
Note generation specific tests |
test_gui.py |
GUI-specific tests |
electron/test/main.test.js |
Electron main process tests (26 cases) |
Run all offline tests: python -m pytest test/ -v -k "not integration"
Run Electron tests: cd electron && npx jest test/
Ad-hoc quality check: python benchmark.py --course <ID> --verbose runs the three quality metrics against every note in a course.