Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 12 additions & 4 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,21 @@
Chirp is a Python CLI whose runtime code lives under `chirp/` with Typer commands in `chirp/cli.py`. Recorder, transcription, note generation, and chat pipelines live in sibling packages (`recorder/`, `transcriber/`, `notes/`, `notes_chat/`, `utils/`) and share Pydantic config from `config/settings.py`. Markdown templates sit in `templates/`, while runtime artifacts land in `to-transcribe/`, `transcription-out/`, and `notes-out/`. Tests mirror the domains in `tests/` (e.g., `tests/test_note_generator.py`), so add new coverage alongside the feature.

## Build, Test & Development Commands
Use `make dev-install` once to sync dependencies with `uv` and install pre-commit hooks. Day-to-day, run `make lint`, `make format`, and `make type-check` before pushing; `make check` chains the validation helpers. Run the full suite with `make test` or `make test-coverage` when you need HTML coverage reports. Use `uv run chirp status` to verify your environment, and `make process` for an end-to-end functional smoke.
**Setup:** `make dev-install` (installs deps with `uv` and pre-commit hooks).
**Quality checks:** `make check` chains `validate`, `lint`, `format-check`, `spell-check`, and `type-check`. Run `make lint-fix` and `make format` to auto-fix issues before pushing.
**Testing:** `make test` runs pytest; `make test-coverage` generates HTML reports. Run a single test with `uv run pytest tests/test_note_generator.py` or a specific function with `uv run pytest tests/test_note_generator.py::test_function_name`. Use `make test-failed` to re-run only failures.
**Validation:** `uv run chirp status` verifies environment; `make process` smoke-tests the full pipeline (record → transcribe → notes).

## Coding Style & Naming Conventions
Ruff enforces formatting (88-char lines, double quotes, spaces for indent), so run `make format` after major edits. Prefer descriptive module-level functions and keep CLI command names terse (see `chirp/cli.py`). Name files and functions after the domain action (`*_manager`, `*_processor`). Follow "Clean Code" guidance: keep logic readable, avoid unnecessary comments, and document only intent that code cannot express. Type hints are encouraged; mypy runs against key packages, so silence warnings with actual annotations instead of `type: ignore`.
**Formatting:** Ruff enforces 88-char lines, double quotes, space indents (run `make format` after edits). Pre-commit hooks auto-fix ruff, codespell, trailing whitespace, and YAML.
**Imports:** Group stdlib, third-party, and first-party (`chirp`, `config`, `notes`, etc.) via isort. Use absolute imports; avoid star imports except for exceptions.
**Naming:** Files and functions follow domain actions (`*_manager`, `*_processor`). CLI commands stay terse (see `chirp/cli.py`). Classes use PascalCase; functions/vars use snake_case.
**Types:** Type hints encouraged; mypy checks `chirp`, `config`, `notes`, `notes_chat`, `recorder`, `transcriber`, `utils`. Silence warnings with annotations, not `type: ignore`.
**Comments:** Avoid unnecessary comments; write self-documenting code. Document only non-obvious intent that code cannot express.
**Error handling:** Raise custom exceptions from `chirp.exceptions` for domain errors; use generic exceptions sparingly.

## Testing Guidelines
Write `pytest` tests in `tests/` with filenames starting `test_` and functions mirroring user-facing behavior. Use fixtures to stub audio and Ollama integrations; see `tests/test_audio_recorder.py` and `tests/notes_chat/` for patterns. When adding asynchronous or long-running flows, include a fast unit test and, if needed, a skipped integration marked with a reason. Keep coverage above existing baselines by running `make test-coverage` locally.
Write `pytest` tests in `tests/` with filenames starting `test_` and functions mirroring user-facing behavior. Use fixtures to stub audio and Ollama integrations; see `tests/test_audio_recorder.py` and `tests/notes_chat/` for patterns. Mark slow or integration tests with `@pytest.mark.slow` or `@pytest.mark.integration`; skip with `@pytest.mark.skip(reason="...")` if needed. Keep coverage above existing baselines by running `make test-coverage` locally.

## Commit & Pull Request Guidelines
Commit subjects follow the short, imperative style already in history (e.g., `Add file name override option`). Group related changes and include meaningful bodies when context is not obvious. Before opening a PR, ensure `make check` and `make test` pass, link any GitHub issues, and describe runtime impacts. Screenshots or sample CLI output help reviewers validate UX changes; attach them when modifying prompts or note templates.
Commit subjects follow the short, imperative style (e.g., `Add file name override option`). Group related changes; include meaningful bodies when context is not obvious. Before opening a PR, ensure `make check` and `make test` pass, link any GitHub issues, and describe runtime impacts. Screenshots or sample CLI output help reviewers validate UX changes; attach them when modifying prompts or note templates.
16 changes: 9 additions & 7 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@

### Core Functionality

- **Audio Recording**: High-quality meeting recording with system audio capture
- **Transcription**: Speech-to-text using OpenAI's Whisper models
- **Audio Recording**: High-quality meeting recording with system audio capture (uses default input device)
- **Live Transcription**: Real-time transcription with scrollable dashboard during recording
- **Transcription**: Speech-to-text using OpenAI's Whisper models (large-v3-turbo)
- **AI Notes**: Generate structured meeting summaries using Ollama LLMs
- **Semantic Search**: Query meeting notes with ChromaDB vector search
- **Interactive Chat**: Ask questions about meeting content
Expand All @@ -26,18 +27,19 @@

- **`chirp/`**: Main CLI module with Typer commands
- **`config/`**: Settings management with Pydantic and platform-specific paths
- **`recorder/`**: Audio recording with PyAudio and device management
- **`transcriber/`**: Whisper-based transcription with batch processing
- **`recorder/`**: Audio recording with PyAudio, device management, live transcription with scrollable dashboard
- **`transcriber/`**: Whisper-based transcription with batch processing and streaming support
- **`notes/`**: AI note generation using Ollama
- **`notes_chat/`**: Semantic search and interactive chat features
- **`utils/`**: Shared utilities for file operations and time handling

### External Dependencies

- **Audio**: PyAudio, PortAudio (system), BlackHole (macOS)
- **AI Models**: Ollama (llama3.1:8b, nomic-embed-text)
- **Audio**: PyAudio, PortAudio (system), optional BlackHole/aggregate devices (macOS)
- **AI Models**: Ollama (llama3.1:8b for notes, nomic-embed-text for search), faster-whisper (large-v3-turbo)
- **Database**: ChromaDB for vector search
- **CLI**: Typer with Rich for beautiful terminal output
- **CLI**: Typer with Rich for beautiful terminal output, including scrollable live dashboard
- **VAD**: WebRTC VAD for speech detection in live transcription

## Development Guidelines

Expand Down
67 changes: 52 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@

## ✨ Features

- **🎙️ System Audio Recording**: Capture high-quality audio from meetings, calls, or any system audio using BlackHole
- **🎙️ System Audio Recording**: Capture high-quality audio from meetings, calls, or any system audio
- **📺 Live Transcription**: Real-time transcription with scrollable dashboard while recording
- **🤖 AI Transcription**: Powered by faster-whisper with Apple Silicon optimization for accurate speech-to-text
- **📋 Smart Note Generation**: Uses Ollama + Llama 3.1 to generate structured meeting notes with key points, decisions, and action items
- **📁 Batch Processing**: Process multiple audio files with progress indicators
Expand All @@ -35,18 +36,28 @@ brew install chirp
chirp setup
```

### Install BlackHole audio driver
### Setup Audio Input Device

Chirp uses your system's default input device for recording. You have several options:

**Option 1: Use an Aggregate Device (Recommended for system audio + microphone)**
- Open Audio MIDI Setup (Applications/Utilities)
- Create an Aggregate Device combining:
- Your microphone (for your voice)
- BlackHole (for system audio)
- Set this aggregate device as your default input in System Settings → Sound

**Option 2: Use BlackHole Only (System audio only)**
```bash
# Install Blackhole
# Install BlackHole
brew install blackhole-2ch
```
- Set BlackHole as your default input device
- Create a Multi-Output Device to hear audio while recording

- Set up a multi-output device using `Audio MIDI Setup`:
- Open Audio MIDI Setup (Applications/Utilities)
- Create a Multi-Output Device
- Include both your speakers and BlackHole
- Set this as your default output device
**Option 3: Use Built-in Microphone**
- Just use your Mac's built-in microphone (no setup needed)
- Set it as default input in System Settings → Sound

### Setup Ollama and AI Models

Expand Down Expand Up @@ -129,10 +140,29 @@ chirp record --duration 30 --title "Client Meeting"
# Record indefinitely (stop with Ctrl+C)
chirp record

# Record with live transcription (real-time transcription while recording)
chirp record --live-transcribe --title "Team Meeting"

# Record with custom settings
chirp record -d 45 -t "Project Planning"
```

#### Live Transcription Mode

When using `--live-transcribe`, you get a real-time dashboard showing:

- **Live transcript**: See the transcription as it happens
- **Audio levels**: Visual feedback of recording volume
- **Speaker detection**: See when speech is detected
- **Scrollable view**: Use arrow keys to scroll through the transcript while recording continues

**Keyboard Controls During Live Transcription**:
- `↑/↓` or `PgUp/PgDn`: Scroll through the transcript
- `Space` or `Enter`: Jump back to latest transcription (resume auto-scroll)
- `Ctrl+C`: Stop recording and save

**Note**: Live transcription uses the `large-v3-turbo` Whisper model and provides near real-time results. The final high-quality transcript can be generated afterward with `chirp transcribe`.

### Processing Audio

```bash
Expand Down Expand Up @@ -248,17 +278,24 @@ chirp status

### Audio Recording Issues

**BlackHole not detected**:
**No audio being recorded**:

1. Download and install BlackHole from [existential.audio/blackhole](https://existential.audio/blackhole/)
2. Set up a multi-output device in Audio MIDI Setup
3. Verify detection: `chirp devices`
1. Check your default input device in System Settings → Sound
2. Verify the device is working: `chirp devices`
3. Check audio permissions in System Preferences > Security & Privacy > Microphone

**Recording fails to start**:

1. Check audio permissions in System Preferences > Security & Privacy > Microphone
2. Verify BlackHole installation: `chirp test`
2. Verify your default input device is set correctly
3. List available devices: `chirp devices`
4. Try a different input device

**Want to record system audio**:

1. Install BlackHole: `brew install blackhole-2ch`
2. Create an Aggregate Device in Audio MIDI Setup
3. Set the aggregate device as your default input

### AI/LLM Issues

Expand Down Expand Up @@ -286,12 +323,12 @@ chirp status

### Near-term

- **Speaker Detection**: Identify different speakers in meetings
- **Speaker Diarization**: Identify and label different speakers in meetings
- **Streaming Transcription**: Segment-by-segment streaming display

### Long-term

- **Calendar Integration**: Auto-trigger recording from macOS Calendar
- **Real-time Transcription**: Live transcription during recording
- **Multiple Export Formats**: PDF, DOCX, Notion, etc.
- **Meeting Analytics**: Insights and patterns from meeting data
- **OS Support**: Support Windows and Linux
Expand Down
128 changes: 109 additions & 19 deletions chirp/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,13 @@
from rich.table import Table

from chirp.exceptions import *
from config.settings import get_settings
from config.settings import ChirpSettings, get_settings
from notes.manual_note_manager import ManualNoteManager
from notes.note_editor import ManualNoteEditor
from notes.note_generator import NoteGenerator
from recorder.audio_recorder import AudioRecorder
from recorder.device_manager import DeviceManager
from recorder.live_session import LiveSessionResult, LiveTranscriptionSession
from transcriber.batch_processor import BatchProcessor
from utils.file_utils import (
get_audio_files,
Expand Down Expand Up @@ -128,17 +129,27 @@ def record(
title: Optional[str] = typer.Option(
None, "--title", "-t", help="Meeting title for filename"
),
live_transcribe: bool = typer.Option(
False,
"--live-transcribe/--no-live-transcribe",
help="Stream live transcription while recording",
),
debug_live: bool = typer.Option(
False,
"--debug-live",
help="Debug live transcription (captures intermediate audio chunks)",
hidden=True,
),
):
"""Start recording a meeting (press Ctrl+C to stop if no duration specified)"""
settings = get_settings()
device_manager = DeviceManager()

if not device_manager.check_blackhole_available():
console.print(
"[red]❌ BlackHole not detected. Please install BlackHole audio driver.[/red]"
if live_transcribe:
_run_live_transcription(
settings, device_manager, title, duration, debug_live=debug_live
)
console.print("Download from: https://existential.audio/blackhole/")
raise typer.Exit(1)
return

recorder = AudioRecorder(settings, device_manager)

Expand Down Expand Up @@ -186,6 +197,46 @@ def record(
raise typer.Exit(1)


def _run_live_transcription(
settings: "ChirpSettings",
device_manager: DeviceManager,
title: Optional[str],
duration: Optional[int],
debug_live: bool = False,
):
if title:
console.print(f"[cyan]📝 Title: {title}[/cyan]")
if duration:
console.print(f"[cyan]⏱️ Planned duration: {duration} minutes[/cyan]")

session = LiveTranscriptionSession(
settings=settings,
device_manager=device_manager,
console=console,
title=title,
duration_minutes=duration,
debug=debug_live,
)

try:
result: LiveSessionResult = session.run()
except RecordingError as e:
console.print(f"[red]❌ Live recording error: {str(e)}[/red]")
raise typer.Exit(1)

from utils.time_utils import format_duration

console.print()
console.print("[green]✅ Live recording complete[/green]")
console.print(f"[dim]Audio saved to:[/dim] {result.audio_path}")
console.print(
f"[dim]Duration:[/dim] {format_duration(result.duration_seconds)} • [dim]Live words transcribed:[/dim] {result.total_words}"
)
console.print(
"[dim]Run 'chirp transcribe' to generate the high-quality transcript.[/dim]"
)


@app.command(rich_help_panel=CHAT_PANEL)
def search():
"""Live search through your note titles"""
Expand Down Expand Up @@ -311,6 +362,9 @@ def transcribe(
force: bool = typer.Option(
False, "--force", "-f", help="Re-transcribe already processed files"
),
stream: bool = typer.Option(
True, "--stream/--no-stream", help="Stream transcription as it processes"
),
):
"""Transcribe audio files to text"""
settings = get_settings()
Expand All @@ -326,18 +380,51 @@ def transcribe(

processor = BatchProcessor(settings)

with Progress(
SpinnerColumn(),
TextColumn("[progress.description]{task.description}"),
console=console,
) as progress:
task = progress.add_task("Transcribing audio files...", total=len(audio_files))
segment_callback = None
if stream:
from rich.live import Live
from rich.text import Text

results = processor.process_files(
audio_files,
force=force,
progress_callback=lambda: progress.update(task, advance=1),
)
streaming_text = Text()

def on_segment(segment):
text = segment.get("text", "").strip()
if text:
streaming_text.append(text + " ", style="cyan")

segment_callback = on_segment

with Live(streaming_text, console=console, refresh_per_second=4):
with Progress(
SpinnerColumn(),
TextColumn("[progress.description]{task.description}"),
console=console,
) as progress:
task = progress.add_task(
"Transcribing audio files...", total=len(audio_files)
)

results = processor.process_files(
audio_files,
force=force,
progress_callback=lambda: progress.update(task, advance=1),
on_segment=segment_callback,
)
else:
with Progress(
SpinnerColumn(),
TextColumn("[progress.description]{task.description}"),
console=console,
) as progress:
task = progress.add_task(
"Transcribing audio files...", total=len(audio_files)
)

results = processor.process_files(
audio_files,
force=force,
progress_callback=lambda: progress.update(task, advance=1),
)

success_count = sum(1 for r in results if r["success"])
processed_count = len(results)
Expand Down Expand Up @@ -543,9 +630,12 @@ def devices():

if device_manager.check_blackhole_available():
console.print("[green]✅ BlackHole detected and ready[/green]")
elif device_manager.check_aggregate_available():
console.print("[green]✅ Aggregate device detected and ready[/green]")
else:
console.print("[red]❌ BlackHole not found[/red]")
console.print("Install from: https://existential.audio/blackhole/")
console.print("[red]❌ No suitable input device found[/red]")
console.print("Install BlackHole from: https://existential.audio/blackhole/")
console.print("Or create an Aggregate Device in Audio MIDI Setup")


@app.command(rich_help_panel=SETUP_PANEL)
Expand Down
2 changes: 1 addition & 1 deletion config/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ def convert_to_path(cls, v):


class ModelsConfig(BaseModel):
whisper: str = "base"
whisper: str = "large-v3-turbo"
llm: str = "llama3.1:8b"
ollama_url: str = "http://localhost:11434"

Expand Down
Loading
Loading