Skip to content

Commit 8c24b84

Browse files
authored
Merge pull request #4 from pcphil/feat/app
publish exe zip
2 parents 8e208b0 + c7cc72e commit 8c24b84

16 files changed

Lines changed: 1013 additions & 8 deletions

.claude/settings.local.json

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,12 @@
1919
"Bash(cd /c/Users/chung/projects/bite-size-notes && .venv/Scripts/ruff.exe check src/ 2>&1)",
2020
"Bash(uv run:*)",
2121
"Bash(uv:*)",
22-
"Bash(rm:*)"
22+
"Bash(rm:*)",
23+
"Bash(grep:*)",
24+
"WebFetch(domain:github.com)",
25+
"WebFetch(domain:dev.to)",
26+
"Bash(.venv/Scripts/python.exe:*)",
27+
"Bash(wc:*)"
2328
]
2429
}
2530
}

CONTRIBUTING.md

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
# Contributing to Bite-Size Notes
2+
3+
## Prerequisites
4+
5+
- Python 3.10+
6+
- [uv](https://docs.astral.sh/uv/) package manager
7+
- Windows 10+ (for system audio capture via WASAPI loopback)
8+
- macOS is supported but requires [BlackHole](https://existential.audio/blackhole/) for system audio
9+
10+
## Getting Started
11+
12+
```bash
13+
# Clone the repo
14+
git clone https://github.com/pcphil/bite-size-notes.git
15+
cd bite-size-notes
16+
17+
# Install all dependencies (including dev tools)
18+
uv sync --extra dev
19+
20+
# Run the application
21+
uv run bite-size-notes
22+
```
23+
24+
## Development Workflow
25+
26+
### Running the App
27+
28+
```bash
29+
uv run bite-size-notes
30+
# or
31+
uv run python -m bite_size_notes
32+
```
33+
34+
### Tests
35+
36+
```bash
37+
# Run all tests
38+
uv run pytest
39+
40+
# Run a specific test
41+
uv run pytest tests/test_foo.py::test_bar -v
42+
```
43+
44+
### Linting & Formatting
45+
46+
```bash
47+
# Check for lint issues
48+
uv run ruff check src/
49+
50+
# Auto-format code
51+
uv run ruff format src/
52+
```
53+
54+
Please run both lint and format before submitting a PR.
55+
56+
## Architecture Overview
57+
58+
```
59+
Audio capture → Queue → Transcription worker → Qt signals → GUI
60+
```
61+
62+
- **AudioCaptureThread** (`audio/capture.py`) — Opens mic + system loopback streams in parallel threads. Flushes `AudioChunk` objects to a shared `queue.Queue`.
63+
- **TranscriberWorker** (`transcription/worker.py`) — `QThread` that pulls chunks from the queue, runs Whisper inference, and emits Qt signals.
64+
- **TranscriptionEngine** (`transcription/engine.py`) — Wrapper around `faster_whisper.WhisperModel`. Expects float32, 16 kHz, mono audio.
65+
- **MainWindow** (`gui/main_window.py`) — Connects signals, manages record/stop lifecycle, displays the transcript.
66+
- **AppConfig** (`utils/config.py`) — Settings persistence via `QSettings`.
67+
68+
### Key Conventions
69+
70+
- Thread communication uses `queue.Queue` (audio threads to QThread) and Qt signals (QThread to GUI). Never call GUI methods from non-Qt threads.
71+
- Audio is always float32, 16 kHz, mono internally.
72+
- Device index `-1` means "use default / auto-detect".
73+
- The queue has a max size of 100; oldest chunks are dropped when full.
74+
75+
### Platform-Specific Code
76+
77+
- **Windows**: System audio via `pyaudiowpatch` (WASAPI loopback). This dependency is conditional in `pyproject.toml`.
78+
- **macOS**: System audio via BlackHole, captured as a regular `sounddevice` input.
79+
80+
## Building Executables
81+
82+
Requires the `dev` extra (`pyinstaller` is included):
83+
84+
```bash
85+
# Build the exe
86+
uv run python build_exe.py
87+
88+
# Build a debug exe (opens a console window for troubleshooting)
89+
uv run python build_exe.py --debug
90+
91+
# Build the exe + Inno Setup installer (requires iscc on PATH)
92+
uv run python build_exe.py --installer
93+
```
94+
95+
Output lands in `dist/bite_size_notes/`. The Inno Setup installer outputs to `dist/BiteSizeNotes_Setup.exe`.
96+
97+
### Debugging a Frozen Build
98+
99+
Use `--debug` to build with a visible console window. If the app crashes on startup, the console stays open and displays the full traceback. Press Enter to dismiss it. A `crash.log` file is also written to `%APPDATA%/Bite-Size Notes/`.
100+
101+
## Submitting Changes
102+
103+
1. Fork the repo and create a feature branch from `main`.
104+
2. Make your changes in small, focused commits.
105+
3. Ensure `uv run ruff check src/` and `uv run pytest` pass.
106+
4. Open a pull request against `main` with a clear description of what changed and why.

README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,17 @@ Open **Settings** (`Ctrl+,`) to configure:
4444
- **Language** — English, Spanish, French, German, Chinese, Japanese, Korean, Portuguese, or auto-detect
4545
- **Chunk duration** — how many seconds of audio to buffer before transcribing (3–30s)
4646

47+
## Building
48+
49+
```bash
50+
# Build a standalone exe (requires dev dependencies)
51+
uv sync --extra dev
52+
uv run python build_exe.py
53+
54+
# Build a debug exe (console stays open on crash for troubleshooting)
55+
uv run python build_exe.py --debug
56+
```
57+
4758
## Development
4859

4960
```bash

build_exe.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
"""Build a Windows exe using PyInstaller and package as a portable zip."""
2+
3+
import argparse
4+
import shutil
5+
import subprocess
6+
import sys
7+
from pathlib import Path
8+
9+
10+
def main():
11+
parser = argparse.ArgumentParser(description="Build Bite-Size Notes exe")
12+
parser.add_argument(
13+
"--debug",
14+
action="store_true",
15+
help="Build with console=True so errors print to a visible terminal",
16+
)
17+
args = parser.parse_args()
18+
19+
spec_path = "packaging/bite_size_notes.spec"
20+
21+
# If --debug, create a temp spec in the same directory with console=True
22+
# (must stay in packaging/ so SPECPATH-relative paths still resolve)
23+
if args.debug:
24+
spec_text = Path(spec_path).read_text(encoding="utf-8")
25+
spec_text = spec_text.replace("console=False", "console=True")
26+
debug_spec = Path("packaging/bite_size_notes_debug.spec")
27+
debug_spec.write_text(spec_text, encoding="utf-8")
28+
spec_path = str(debug_spec)
29+
print("DEBUG BUILD: using console=True")
30+
31+
# Step 1: Run PyInstaller
32+
cmd = [
33+
sys.executable,
34+
"-m",
35+
"PyInstaller",
36+
spec_path,
37+
"--noconfirm",
38+
]
39+
print(f"Running: {' '.join(cmd)}")
40+
ret = subprocess.call(cmd)
41+
42+
# Clean up debug spec if we created one
43+
if args.debug:
44+
Path(spec_path).unlink(missing_ok=True)
45+
46+
if ret != 0:
47+
raise SystemExit(ret)
48+
49+
# Step 2: Create portable zip from the dist folder
50+
dist_dir = Path("dist/bite_size_notes")
51+
if not dist_dir.is_dir():
52+
print(f"ERROR: {dist_dir} not found after build.", file=sys.stderr)
53+
raise SystemExit(1)
54+
55+
zip_name = "BiteSizeNotes_v0.1.0"
56+
zip_path = shutil.make_archive(f"dist/{zip_name}", "zip", "dist", "bite_size_notes")
57+
print(f"Created: {zip_path}")
58+
59+
60+
if __name__ == "__main__":
61+
main()

docs/architecture.md

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
# Architecture Overview
2+
3+
Bite-Size Notes is a desktop meeting transcriber that captures microphone and system audio simultaneously, runs local Whisper speech-to-text (via faster-whisper), summarizes transcripts with a local LLM (Qwen3-4B via llama-cpp), and displays a live color-coded transcript. Built with PySide6 (Qt) and Python 3.10+.
4+
5+
**Tech stack**: PySide6, sounddevice, pyaudiowpatch (Windows), faster-whisper, llama-cpp-python, huggingface-hub, NumPy.
6+
7+
## Directory Structure
8+
9+
```
10+
src/bite_size_notes/
11+
├── __init__.py # Package root, declares __version__
12+
├── __main__.py # `python -m bite_size_notes` entry point
13+
├── app.py # main() bootstrap, QApplication, crash handler
14+
├── assets/
15+
│ ├── logo.ico # App icon (runtime & PyInstaller)
16+
│ └── readme.png # Screenshot for README
17+
18+
├── audio/ # Audio capture subsystem
19+
│ ├── __init__.py
20+
│ ├── capture.py # AudioCaptureThread, AudioChunk
21+
│ ├── devices.py # Device enumeration (mic, loopback)
22+
│ └── mixer.py # mix_audio() utility
23+
24+
├── gui/ # Qt user interface
25+
│ ├── __init__.py
26+
│ ├── main_window.py # MainWindow, _ModelPreloadThread, _SummarizeThread
27+
│ ├── transcript_view.py # TranscriptView (center panel)
28+
│ ├── chat_bubble.py # TranscriptLineWidget, _AutoResizePlainTextEdit
29+
│ ├── sidebar_panel.py # SidebarPanel, _SessionItemWidget
30+
│ ├── output_panel.py # OutputPanel (summary display)
31+
│ ├── notes_panel.py # NotesPanel (floating overlay)
32+
│ ├── settings_dialog.py # SettingsDialog, model download threads
33+
│ ├── export_dialog.py # export_transcript(), export_output()
34+
│ └── themes.py # Dark/Light palettes, stylesheet builder
35+
36+
├── models/ # Data models & persistence
37+
│ ├── __init__.py
38+
│ ├── transcript.py # TranscriptSegment, TranscriptSession
39+
│ └── session_store.py # SessionStore (filesystem persistence)
40+
41+
├── summarization/ # LLM summarization
42+
│ ├── __init__.py
43+
│ └── engine.py # Qwen3-4B GGUF via llama-cpp-python
44+
45+
├── transcription/ # Speech-to-text
46+
│ ├── __init__.py
47+
│ ├── engine.py # TranscriptionEngine (faster-whisper wrapper)
48+
│ ├── worker.py # TranscriberWorker (QThread)
49+
│ └── model_utils.py # Model cache checks & downloads
50+
51+
└── utils/ # Shared utilities
52+
├── __init__.py
53+
├── config.py # AppConfig (QSettings wrapper)
54+
└── platform.py # is_windows(), is_macos()
55+
```
56+
57+
## Module Responsibilities
58+
59+
### `audio/`
60+
61+
Handles all audio input. `AudioCaptureThread` opens two parallel streams — one for the microphone (via sounddevice) and one for system/loopback audio (WASAPI on Windows, BlackHole on macOS). It accumulates audio in per-stream buffers, runs silence detection, and flushes `AudioChunk` dataclass objects into a shared `queue.Queue`. `devices.py` provides device enumeration and auto-detection helpers. `mixer.py` contains a utility for mixing two audio arrays (zero-pad + normalize).
62+
63+
### `transcription/`
64+
65+
Converts audio chunks to text. `TranscriptionEngine` wraps `faster_whisper.WhisperModel` and runs inference with VAD filtering. `TranscriberWorker` is a `QThread` that pulls `AudioChunk` objects from the shared queue, calls the engine, and emits `transcription_ready` Qt signals with speaker label, timestamp, and text. `model_utils.py` provides helpers to check if a Whisper model is cached and to download one.
66+
67+
### `summarization/`
68+
69+
Generates meeting summaries from transcript text. Uses the Qwen3-4B-Q4_K_M GGUF model via `llama-cpp-python`. The module provides `load_summarizer()` to download/load the model and `summarize()` to run inference with a structured system prompt that produces formatted meeting notes.
70+
71+
### `gui/`
72+
73+
All Qt UI code. `MainWindow` orchestrates the application — managing the record/stop lifecycle, connecting signals between audio/transcription threads and the UI, and handling session management. The layout is a horizontal `QSplitter` with three panels: `SidebarPanel` (session list), `TranscriptView` (live transcript with editable chat bubbles), and `OutputPanel` (summary display). `NotesPanel` is a floating overlay for user notes. `SettingsDialog` manages preferences and model downloads. `themes.py` provides dark/light/system theme support.
74+
75+
### `models/`
76+
77+
Data structures and persistence. `TranscriptSegment` represents a single transcribed utterance (text, source, timestamp, speaker). `TranscriptSession` aggregates segments into a session with title, summary, and export methods (text, SRT, Markdown, JSON). `SessionStore` manages reading/writing session JSON files to the OS-specific app data directory.
78+
79+
### `utils/`
80+
81+
Shared configuration and platform detection. `AppConfig` wraps `QSettings` with typed properties for all user preferences (devices, model size, language, theme). `platform.py` provides `is_windows()` and `is_macos()` helpers used throughout the codebase.
82+
83+
## Class Hierarchy
84+
85+
### Audio
86+
87+
| Class | Base | Role |
88+
|---|---|---|
89+
| `AudioChunk` | `dataclass` | Data container: `data` (float32 ndarray), `source` ("mic"/"loopback"), `timestamp`, `sample_rate` |
90+
| `AudioCaptureThread` | `threading.Thread` | Opens mic + loopback streams, silence detection, flushes chunks to queue |
91+
| `AudioDevice` | `dataclass` | Device descriptor: `index`, `name`, `max_input_channels`, `default_samplerate`, `is_loopback` |
92+
93+
### Transcription
94+
95+
| Class | Base | Role |
96+
|---|---|---|
97+
| `TranscriptionEngine` || Wraps `faster_whisper.WhisperModel`; `transcribe(audio) -> list[dict]` |
98+
| `TranscriberWorker` | `QThread` | Pulls chunks from queue, runs engine, emits `transcription_ready` signal |
99+
100+
### Summarization
101+
102+
| Function | Role |
103+
|---|---|
104+
| `load_summarizer()` | Downloads/loads Qwen3-4B GGUF, returns `Llama` instance |
105+
| `summarize(llm, text)` | Runs chat completion, strips think blocks, returns summary string |
106+
| `is_summarizer_cached()` | Checks HF cache for the GGUF file |
107+
| `download_summarizer_sync()` | Downloads the GGUF via `hf_hub_download` |
108+
109+
### GUI
110+
111+
| Class | Base | Role |
112+
|---|---|---|
113+
| `MainWindow` | `QMainWindow` | Top-level window; orchestrates recording, transcription, summarization, sessions |
114+
| `TranscriptView` | `QWidget` | Scrollable list of transcript chat bubbles |
115+
| `TranscriptLineWidget` | `QFrame` | Single transcript entry: timestamp + speaker label + editable text |
116+
| `_AutoResizePlainTextEdit` | `QPlainTextEdit` | Text edit that auto-sizes to content height |
117+
| `SidebarPanel` | `QWidget` | Session list with new/rename/delete actions |
118+
| `_SessionItemWidget` | `QWidget` | Single session row in the sidebar |
119+
| `OutputPanel` | `QWidget` | Summary display with "Bite Size It" button, copy/export controls |
120+
| `NotesPanel` | `QFrame` | Floating 300x250 overlay for user notes |
121+
| `SettingsDialog` | `QDialog` | Preferences: theme, devices, model size, language, summarizer |
122+
| `_ModelPreloadThread` | `QThread` | Loads `TranscriptionEngine` in background at startup |
123+
| `_SummarizeThread` | `QThread` | Runs summarization in background |
124+
| `_ModelDownloadThread` | `QThread` | Downloads Whisper model (from settings dialog) |
125+
| `_SummarizerDownloadThread` | `QThread` | Downloads GGUF model (from settings dialog) |
126+
127+
### Models
128+
129+
| Class | Base | Role |
130+
|---|---|---|
131+
| `TranscriptSegment` | `dataclass` | Single utterance: `text`, `source`, `timestamp`, `speaker_label` |
132+
| `TranscriptSession` | `dataclass` | Full session: segments list, title, summary, start_time, UUID; export methods |
133+
| `SessionStore` || Filesystem CRUD for session JSON files in app data directory |
134+
135+
### Utils
136+
137+
| Class | Base | Role |
138+
|---|---|---|
139+
| `AppConfig` || `QSettings` wrapper with typed properties for all user preferences |
140+
141+
---
142+
143+
See also: [Data Flow](data-flow.md) | [Platform Guide](platform-guide.md)

0 commit comments

Comments
 (0)