Local tooling for estimating piano practice time from MP3 and WAV recordings. ffprobe reads metadata and FFmpeg's silencedetect filter streams decoded audio to find long silent intervals. The MVP does not identify pieces or individual notes and never sends files over the network.
- Linux and Python >= 3.12
uv,ffmpeg, andffprobeavailable onPATH
uv sync --all-groups
uv run piano-practice doctorIf tools are missing on Arch/Manjaro, install them deliberately (for example, sudo pacman -S uv ffmpeg). The project itself never invokes sudo or ordinary pip.
uv run piano-practice run \
--input "/path/to/recordings" \
--output "./piano-practice-output" \
--from 2025-01-01 \
--to 2026-12-31 \
--silence-min 60 \
--silence-db -45
xdg-open ./piano-practice-output/report/report.htmlrun recursively scans the configured audio extensions (.mp3 and .wav by default) and writes SQLite, CSV, JSON, a log, and an offline HTML report. A bad individual file does not abort the run; exit code 2 means that the report is partial. --strict turns that result into exit code 1.
uv run piano-practice init-config --output piano-practice.tomlThe generated, commented file contains [input], [date], [audio], [processing], and [report] sections. CLI arguments override TOML values. doctor --config piano-practice.toml validates ranges, IANA timezone data, directories, and system tools.
The generated configuration scans both MP3 and WAV files. Set input.extensions explicitly when a run should include only one format.
The key parameter is silence_min_seconds (--silence-min). Silence shorter than the threshold remains counted; silence equal to or longer than the threshold is excluded in full. silence_threshold_db (--silence-db) controls FFmpeg sensitivity. Active time is an estimate, not a note-level measurement.
date.max_active_day_seconds defaults to 21600 (6 hours). If the unioned active time assigned to one local date exceeds this limit, that date is treated as unreliable: its audio remains in overall totals, but is removed from calendar, daily, monthly, hourly, weekday, heatmap, streak, and session statistics. Use --max-active-day-hours to override it for a run.
The resolver compares a CSV override, ID3 TDRC, supported filename patterns, filesystem birth time, and mtime - duration. The recording-time estimate mtime - duration is the primary filesystem source; birth time is diagnostic and is downweighted when it looks like a later copy operation. A valid filename or ID3 timestamp can become the fallback when the filesystem estimate conflicts with it. Candidates retain source, range status, weight, and warnings; agreeing sources form deterministic clusters. Linux ctime and atime are never used. Ambiguous DST local times are retained diagnostically and resolved deterministically with fold=0.
When a date is uncertain, edit date-review.csv or date-overrides.csv:
fingerprint,path,status,chosen_start,...,override_start,override_note
The fingerprint is the key, so renaming a file does not invalidate an override. Invalid overrides are reported as invalid_override.
state/analysis.sqlite3 stores the manifest, date candidates, issues, results, and silence-analysis cache. The cache key depends on content fingerprint, size, analyzer version, FFmpeg version, silence threshold, dB threshold, and channel mode. Changing report styling does not decode MP3 again; changing a silence parameter invalidates the relevant entries.
processing.workers (or --workers) bounds concurrent FFmpeg jobs and is capped by the available CPU count.
Exact duplicates are confirmed with SHA-256 after a fast BLAKE2b fingerprint and are not counted twice. Overlapping absolute intervals are unioned before aggregation.
Every run has its own ID and summary. Re-running the same input preserves run history, reuses unchanged audio from the cache, and calculates totals only from the files found in that run. The Rich summary and HTML header show the oldest and newest detected recording dates.
report/report.html is static, responsive, and self-contained: SVG charts are embedded inline, with no JavaScript, CDN, or network font. Its dark Tokyo Night palette uses the report’s analytical content as the accent rather than decorative UI chrome. It includes daily and cumulative time, 7/30-day rolling averages, monthly totals, a calendar heatmap, hour distributions, a weekday × hour heatmap, session histograms and starts, KPI cards, methodology, and data-quality issues. Silence totals remain available in summary.json and detailed interval CSVs, but are not plotted as a monthly chart.
report/data/ contains stable CSV files (files, date-candidates, date-review, date-anomalies, silence-intervals, active-intervals, sessions, daily, monthly, issues, duplicates) plus versioned summary.json.
For privacy, report HTML, CSV, and JSON exports contain filenames only, never the absolute input directory. Full paths remain available only in the local SQLite state and technical logs needed for cache and diagnostics.
Open the sanitized static demo report directly in a browser. It is a single offline HTML file with inline SVG charts and no network dependencies. The terminal output below shows a completed run with cache reuse:
Regenerate the report without decoding audio:
uv run piano-practice report \
--state ./piano-practice-output/state/analysis.sqlite3 \
--output ./piano-practice-outputDiagnose one file:
uv run piano-practice inspect recording.mp3 --silence-min 60 --silence-db -45run --json emits stable JSONL events (file_started, file_completed, file_failed, run_completed) without ANSI. --no-progress disables the Rich progress bar. Normal runs show separate prepared and completed counts, so probing/queueing is not mistaken for active FFmpeg decoding, followed by a final table.
uv run python scripts/generate_demo_recordings.py --output ./demo-recordings
uv run piano-practice run --input ./demo-recordings --output ./demo-output \
--from 2025-01-01 --to 2026-12-31 --silence-min 1 --silence-db -45
uv run ruff check .
uv run ruff format --check .
uv run mypy src
uv run pytest
uv run pytest --cov=src/piano_practice --cov-report=term-missingFFmpeg integration tests generate short synthetic data in a temporary directory and cover silence boundaries, stereo, VBR-compatible decoding, and unreadable input without committing binary fixtures.
Source MP3 files are read-only. FFmpeg, mutagen, SQLite, and chart generation run locally; the application has no network client. Results depend on acoustics, codec behavior, dB threshold, and metadata. Birth time may be unavailable on Linux, and unreliable dates are skipped rather than guessed.
The AnalysisPlugin extension point is disabled by default and stores namespaced, versioned JSON. Future plugins can inspect active intervals (for example, exercise type), but the MVP has no AI models and no dynamic plugin download.
