Skip to content

Commit f05af9a

Browse files
authored
Merge pull request #20 from mariuspruvot/feat/history-and-polish
feat: transcription history + output polish
2 parents 1e3782d + 15a0625 commit f05af9a

32 files changed

Lines changed: 4409 additions & 158 deletions

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Project-specific
22
.roadmap.md
3-
CLAUDE.md
3+
44
# Python
55
__pycache__/
66
*.py[cod]

CLAUDE.md

Lines changed: 356 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,356 @@
1+
# CLAUDE.md - shh Project
2+
3+
This file provides **project-specific** guidance to Claude Code when working on the **shh** voice transcription CLI.
4+
5+
For general Python development preferences, see `~/.claude/CLAUDE.md`. **This project file overrides global preferences where specified.**
6+
7+
---
8+
9+
## Project Overview
10+
11+
**shh** is a voice transcription CLI powered by OpenAI Whisper. It allows users to record audio from their terminal, transcribe it using the Whisper API, and optionally format or translate the output using AI.
12+
13+
**Core Features**:
14+
- Record audio from microphone or transcribe existing files
15+
- Format transcriptions with AI (casual, business, or neutral style)
16+
- Translate transcriptions to any language
17+
- Copy results to clipboard automatically
18+
- Async architecture for responsive UX
19+
20+
**Tech Stack**: Python 3.11+ • OpenAI Whisper • PydanticAI (`gpt-4o-mini`) • Typer • Rich • prompt_toolkit (transitive via ipython, used for the history picker) • sounddevice
21+
22+
---
23+
24+
## Architecture
25+
26+
This project follows a **Pragmatic Layered Architecture**. The CLI calls a thin service layer that orchestrates the recording/transcription/formatting flow.
27+
28+
```
29+
CLI (Typer)
30+
├─ Output UI: Rich / Quiet / Pipe (TTY-aware selection)
31+
└─ History picker (prompt_toolkit, shh history)
32+
33+
Services Layer RecordingService
34+
35+
┌───────┴────────┐
36+
Core Adapters
37+
(models, (audio, whisper,
38+
styles) llm, clipboard, history)
39+
```
40+
41+
**Dependency Rule**: `CLI → Services → (Core + Adapters)`. Lower layers never import from upper layers. Adapters are framework-agnostic and never import `cli/` or `services/`.
42+
43+
### Directory Structure
44+
45+
```
46+
shh/
47+
├── cli/ # CLI Layer - Typer commands + UI abstraction
48+
│ ├── app.py # Typer app entry point (callback pattern, default = record)
49+
│ ├── commands/ # Subcommands: record (default), setup, config, history
50+
│ └── ui/ # UIOutput Protocol + RichUI / QuietUI / PipeUI + history_picker
51+
├── services/ # Orchestration layer
52+
│ └── recording.py # RecordingService (record + transcribe + format + persist)
53+
├── core/ # Domain models & enums (no I/O, framework-agnostic)
54+
│ ├── models.py # RecordingOptions, TranscriptionOutput, HistoryEntry, WhisperTranscription
55+
│ └── styles.py # TranscriptionStyle enum
56+
├── adapters/ # External Integrations (framework-agnostic)
57+
│ ├── audio/ # sounddevice recording + scipy WAV processing
58+
│ ├── whisper/ # OpenAI Whisper API client (AsyncOpenAI, verbose_json)
59+
│ ├── llm/ # PydanticAI formatting agent (gpt-4o-mini)
60+
│ ├── clipboard/ # pyperclip wrapper
61+
│ └── history/ # JSONL-backed HistoryStore with size-bounded rotation
62+
├── config/ # Configuration management
63+
│ └── settings.py # pydantic-settings (env + JSON config file)
64+
└── utils/ # Shared utilities
65+
├── exceptions.py # Custom exceptions
66+
└── logger.py # rich.logging setup
67+
```
68+
69+
### Key Architectural Principles
70+
71+
1. **Async-first**: All I/O operations use `async/await` (Whisper API, LLM calls, recording service).
72+
2. **Type safety**: Full type hints + `mypy --strict` enforcement.
73+
3. **Service layer for orchestration**: Record/transcribe/format flow lives in `RecordingService`, not in a CLI command. Commands stay thin.
74+
4. **Protocol-based UI**: Output is abstracted through the `UIOutput` Protocol (`shh/cli/ui/base.py`). New UIs (e.g., `QuietUI`, `RichUI`) implement the methods structurally — no inheritance required.
75+
5. **Framework-agnostic core/adapters/services**: No Typer / Rich imports outside `cli/`.
76+
6. **Temporary files**: Audio recorded to temp WAV, deleted immediately after transcription (`try/finally`).
77+
78+
---
79+
80+
## Development Commands
81+
82+
### Environment Setup
83+
84+
```bash
85+
# Create virtual environment and install
86+
uv venv
87+
source .venv/bin/activate # Windows: .venv\Scripts\activate
88+
uv pip install -e ".[dev]"
89+
90+
# Setup API key
91+
shh setup
92+
```
93+
94+
### Running the CLI
95+
96+
```bash
97+
# Quick record (press Ctrl+C to stop or enter)
98+
shh
99+
100+
# Record with duration
101+
shh record --duration 60
102+
103+
# Format with style
104+
shh --style business
105+
106+
# Translate
107+
shh --translate fr
108+
109+
# Minimal output (for scripts / piping)
110+
shh --quiet
111+
112+
# Verbose output (overrides quiet_mode config)
113+
shh --verbose
114+
115+
# Skip persisting this transcription to history
116+
shh --no-history
117+
118+
# Browse past transcriptions (interactive picker; Enter = copy, Esc = quit)
119+
shh history
120+
121+
# Purge all history entries (with confirmation)
122+
shh history clear
123+
```
124+
125+
### Testing
126+
127+
**Recommended (using Poe):**
128+
```bash
129+
uv run poe test # Run all tests
130+
uv run poe test-cov # Run with coverage
131+
uv run poe test-unit # Run only unit tests
132+
uv run poe test-integration # Run only integration tests
133+
uv run poe test-no-e2e # Skip E2E tests (no real API calls)
134+
```
135+
136+
**Direct pytest commands:**
137+
```bash
138+
pytest # Run all tests
139+
pytest --cov=shh --cov-report=html --cov-report=term # With coverage
140+
pytest tests/unit/test_formatting.py # Specific test file
141+
pytest -m "not e2e" # Excluding E2E tests
142+
pytest -v # Verbose output
143+
```
144+
145+
### Code Quality
146+
147+
This project uses **Poe the Poet** for task automation. All tasks are run via `uv run poe <task>`.
148+
149+
```bash
150+
# Type checking (strict mode enforced)
151+
uv run poe type # mypy --strict shh/
152+
153+
# Linting
154+
uv run poe lint # ruff check .
155+
uv run poe lint-fix # ruff check . --fix
156+
157+
# Formatting
158+
uv run poe format # ruff format .
159+
160+
# Combined quality check (before commit)
161+
uv run poe check # Runs: type + lint + test
162+
163+
# Auto-fix and format
164+
uv run poe fix # Runs: lint-fix + format
165+
```
166+
167+
**Direct commands (if needed):**
168+
```bash
169+
mypy --strict shh/
170+
ruff check .
171+
ruff check . --fix
172+
```
173+
174+
### Development Utilities
175+
176+
```bash
177+
# Clean cache directories
178+
uv run poe clean
179+
180+
# Install in editable mode
181+
uv pip install -e .
182+
183+
# Install with dev dependencies
184+
uv pip install -e ".[dev]"
185+
```
186+
187+
---
188+
189+
## Configuration
190+
191+
### Config File Location
192+
193+
Platform-specific paths via `platformdirs`:
194+
- **macOS**: `~/Library/Application Support/shh/config.json`
195+
- **Linux**: `~/.config/shh/config.json`
196+
- **Windows**: `%APPDATA%\shh\config.json`
197+
198+
### Config Structure
199+
200+
```json
201+
{
202+
"openai_api_key": "sk-...",
203+
"default_output": ["clipboard", "stdout"],
204+
"show_progress": true,
205+
"default_style": "neutral",
206+
"default_translation_language": null,
207+
"quiet_mode": false,
208+
"whisper_model": "whisper-1",
209+
"history_enabled": true,
210+
"history_retention": 200
211+
}
212+
```
213+
214+
`quiet_mode` selects `QuietUI` over `RichUI` for `shh` invocations on a TTY. The CLI flags `--quiet` / `--verbose` override this field at runtime. When stdout is not a TTY, `PipeUI` is selected regardless. See `shh/config/settings.py` for the authoritative schema.
215+
216+
History persists to `<config_dir>/history.jsonl` next to `settings.json`. `history_retention` is the maximum entry count before rotation (rewrite with the most recent N). `history_enabled = false` disables persistence globally; `--no-history` skips a single invocation.
217+
218+
### Priority Order
219+
220+
1. CLI flags (highest)
221+
2. Environment variables (`SHH_*` prefix)
222+
3. Config file
223+
4. Defaults (lowest)
224+
225+
---
226+
227+
## Implementation Patterns
228+
229+
### Audio Recording
230+
231+
- **Library**: `sounddevice` for cross-platform recording
232+
- **Format**: WAV (16kHz sample rate, optimal for Whisper)
233+
- **Storage**: Temporary files only, deleted after transcription
234+
- **Modes**: Duration-based (`--duration 60`) or interactive (Ctrl+C to stop)
235+
236+
### Whisper Transcription
237+
238+
- **Client**: `AsyncOpenAI` (from the `openai` SDK)
239+
- **Adapter**: `shh/adapters/whisper/client.py` — uploads the temp WAV, returns the raw transcript.
240+
- **Model**: `whisper-1` (configurable via `Settings.whisper_model`).
241+
242+
### PydanticAI Formatting
243+
244+
- **Agent**: `pydantic_ai.Agent` with `OpenAIChatModel("gpt-4o-mini")` (`shh/adapters/llm/formatter.py:103`).
245+
- **Styles**:
246+
- `neutral`: No LLM call **unless** a `target_language` is provided — text is returned as-is from Whisper.
247+
- `casual`: Conversational tone, removes filler words.
248+
- `business`: Formal tone, structured paragraphs.
249+
- **Output**: Structured `FormattedTranscription` Pydantic model.
250+
- **Errors**: Wrapped in `FormattingError` and re-raised at the adapter boundary.
251+
252+
### UI Output Layer (Protocol)
253+
254+
- **Defined in** `shh/cli/ui/base.py`. `UIOutput` is a `typing.Protocol` — structural typing, no inheritance.
255+
- **Implementations**:
256+
- `RichUI` — default. A **single** `Live(transient=True)` spinner morphs through `Recording → Transcribing → [Formatting]` and erases on stop. Final output is plain text + dim `✓ copied to clipboard` (no Panel).
257+
- `QuietUI` — minimal: progress bar + final text.
258+
- `PipeUI` — non-TTY: writes only the transcribed text to stdout; errors/warnings to stderr.
259+
- **TTY-aware selection** (`shh/cli/commands/record.py::_select_ui`):
260+
- `not sys.stdout.isatty()``PipeUI` (overrides every other flag — pipes always get clean text).
261+
- `--quiet` or (`Settings.quiet_mode` and not `--verbose`) → `QuietUI`.
262+
- Otherwise → `RichUI`.
263+
- **Adding a new UI**: implement every method of `UIOutput` (`show_error`, `show_recording_progress`, `show_result`, `cleanup`, …) in a new class — no base class to extend.
264+
265+
### Transcription History
266+
267+
- **Store**: `shh/adapters/history/store.py``HistoryStore(path, retention)`. JSONL, append-only, size-bounded rotation (keeps last N lines after append). Malformed lines on read are skipped with a logger warning.
268+
- **Model**: `HistoryEntry` in `shh/core/models.py` (`id`, `ts`, `text`, `style`, `translate_to`, `duration_s`, `detected_lang`).
269+
- **Persistence**: `RecordingService` appends one entry per successful transcription when `settings.history_enabled and not skip_history`. `OSError` on append is logged and swallowed — a successful transcription should never be lost because history failed to persist.
270+
- **`--no-history` flag** on `shh` / record path threads as `skip_history=True` through the service.
271+
- **Picker UI**: `shh/cli/ui/history_picker.py``prompt_toolkit` `Application` with list + preview + footer. Pure helpers (`filter_entries`, `format_relative_time`, `truncate_text`, `render_row`) are testable in isolation; the interactive layer is not unit-tested.
272+
- **Commands**: `shh history` opens the picker; `shh history clear` purges with confirmation.
273+
274+
### CLI Entry Point Pattern
275+
276+
- `shh/cli/app.py` uses Typer's `@app.callback(invoke_without_command=True)` so that the bare `shh` command runs the default record flow while still allowing `shh setup`, `shh config`, and `shh history` subcommands.
277+
- Whisper detected language is extracted via the SDK's `verbose_json` overload — the response is annotated `TranscriptionVerbose` and `.language` is accessed directly (no `getattr` defensiveness).
278+
279+
### Error Handling
280+
281+
- **Philosophy**: Fail fast with clear error messages
282+
- **Custom exceptions**: Defined in `utils/exceptions.py`
283+
- **API errors**: Translated at adapter boundaries, never leaked to core
284+
- **Cleanup**: Temp files deleted in `try/finally` blocks
285+
286+
---
287+
288+
## Critical Constraints
289+
290+
### MUST Do
291+
292+
1. **Full type hints**: Every function must have complete type annotations.
293+
2. **Pass mypy strict**: `mypy --strict shh/` must pass before committing.
294+
3. **Clean up temp files**: Use `try/finally` to ensure WAV files are deleted.
295+
4. **Layer boundaries**: `CLI → Services → (Core + Adapters)`. Never the reverse.
296+
5. **Structured outputs**: Use Pydantic models for all LLM responses.
297+
6. **Orchestration in services**: New record/transcribe/format logic goes in `services/`, not in a command.
298+
299+
### MUST NOT Do
300+
301+
- Import `typer` / `rich` in `core/`, `adapters/`, or `services/`.
302+
- Import `cli/` from `services/`, `core/`, or `adapters/`.
303+
- Put orchestration logic directly in a Typer command — call `RecordingService` instead.
304+
- Leave temp WAV files on disk after transcription.
305+
- Skip type hints or use `Any` without justification.
306+
- Commit code that fails `mypy --strict` or `ruff check`.
307+
308+
---
309+
310+
### Adding a New Feature
311+
312+
1. **Plan**: Identify which layer owns the logic (CLI / Services / Core / Adapters).
313+
2. **Write tests first**: Unit tests for services and adapters, integration tests for end-to-end flows.
314+
3. **Implement**: Follow layer boundaries strictly. Orchestration belongs in `services/`.
315+
4. **Type check**: Run `uv run poe type`.
316+
5. **Lint**: Run `uv run poe lint` (or `uv run poe fix` to auto-fix).
317+
6. **Test**: Run `uv run poe test` to ensure all tests pass.
318+
7. **Document**: Update CLAUDE.md if architectural changes were made.
319+
320+
### Before Committing
321+
322+
```bash
323+
# Full quality check (type + lint + test)
324+
uv run poe check
325+
326+
# If all pass, you're good to commit
327+
git add .
328+
git commit -m "feat: description"
329+
```
330+
331+
**Alternative (manual commands):**
332+
```bash
333+
mypy --strict shh/ && ruff check . && pytest
334+
```
335+
336+
337+
## Roadmap Reference
338+
339+
See `.roadmap.md` for:
340+
- Detailed architectural decisions
341+
- Complete implementation checklist
342+
- Phase-by-phase development plan
343+
- Open questions and design philosophy
344+
345+
**Note**: `.roadmap.md` is gitignored and contains extensive planning details. This CLAUDE.md focuses on practical development guidance.
346+
347+
---
348+
349+
## References
350+
351+
- [PydanticAI Documentation](https://ai.pydantic.dev/)
352+
- [Typer Documentation](https://typer.tiangolo.com/)
353+
- [OpenAI Whisper API](https://platform.openai.com/docs/guides/speech-to-text)
354+
- [Rich Documentation](https://rich.readthedocs.io/)
355+
- [sounddevice Documentation](https://python-sounddevice.readthedocs.io/)
356+
- Use Context7 mcp for access to documentations

0 commit comments

Comments
 (0)