diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c8248bd..a418c5b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,9 +17,18 @@ jobs: python -m pip install --upgrade pip python -m pip install uv build uv sync --extra x11 - - name: Compile - run: python -m py_compile src/*.py tests/*.py - - name: Unit tests - run: python -m unittest discover -s tests -p 'test_*.py' - - name: Build artifacts - run: python -m build + - name: Release quality checks + run: make release-check + - name: Build Debian package + run: make package-deb + - name: Build Arch package inputs + run: make package-arch + - name: Upload packaging artifacts + uses: actions/upload-artifact@v4 + with: + name: aman-artifacts + path: | + dist/*.whl + dist/*.tar.gz + dist/*.deb + dist/arch/PKGBUILD diff --git a/.gitignore b/.gitignore index 75c9b0b..ae72627 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,5 @@ __pycache__/ *.pyc outputs/ models/ +build/ +dist/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 5284173..7c37d25 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,17 @@ All notable changes to Aman will be documented in this file. The format is based on Keep a Changelog and this project follows Semantic Versioning. +## [Unreleased] + +### Added +- Packaging scripts and templates for Debian (`.deb`) and Arch (`PKGBUILD` + source tarball). +- Make targets for build/package/release-check workflows. +- Persona and distribution policy documentation. + +### Changed +- README now documents package-first installation for non-technical users. +- Release checklist now includes packaging artifacts. + ## [0.1.0] - 2026-02-26 ### Added diff --git a/Makefile b/Makefile index 2bc0610..adf873f 100644 --- a/Makefile +++ b/Makefile @@ -1,10 +1,13 @@ +PYTHON ?= python3 CONFIG := $(HOME)/.config/aman/config.json - -.PHONY: run doctor self-check install sync test check +DIST_DIR := $(CURDIR)/dist +BUILD_DIR := $(CURDIR)/build RUN_ARGS := $(wordlist 2,$(words $(MAKECMDGOALS)),$(MAKECMDGOALS)) RUN_CONFIG := $(if $(RUN_ARGS),$(abspath $(firstword $(RUN_ARGS))),$(CONFIG)) +.PHONY: run doctor self-check sync test check build package package-deb package-arch release-check install-local install-service install clean-dist clean-build clean + ifneq ($(filter run,$(firstword $(MAKECMDGOALS))),) .PHONY: $(RUN_ARGS) $(RUN_ARGS): @@ -24,14 +27,43 @@ sync: uv sync test: - python3 -m unittest discover -s tests -p 'test_*.py' + $(PYTHON) -m unittest discover -s tests -p 'test_*.py' check: - python3 -m py_compile src/*.py + $(PYTHON) -m py_compile src/*.py $(MAKE) test -install: - uv pip install --user . +build: + $(PYTHON) -m build --no-isolation + +package: package-deb package-arch + +package-deb: + ./scripts/package_deb.sh + +package-arch: + ./scripts/package_arch.sh + +release-check: + $(PYTHON) -m py_compile src/*.py tests/*.py + $(MAKE) test + $(MAKE) build + +install-local: + $(PYTHON) -m pip install --user ".[x11]" + +install-service: + mkdir -p $(HOME)/.config/systemd/user cp systemd/aman.service $(HOME)/.config/systemd/user/aman.service systemctl --user daemon-reload systemctl --user enable --now aman + +install: install-local install-service + +clean-dist: + rm -rf $(DIST_DIR) + +clean-build: + rm -rf $(BUILD_DIR) + +clean: clean-build diff --git a/README.md b/README.md index 264b1c8..95fa1a7 100644 --- a/README.md +++ b/README.md @@ -3,23 +3,61 @@ Python X11 STT daemon that records audio, runs Whisper, applies local AI cleanup, and injects text. -## Requirements +## Target User + +The canonical Aman user is a desktop professional who wants dictation and +rewriting features without learning Python tooling. + +- End-user path: native OS package install. +- Developer path: Python/uv workflows. + +Persona details and distribution policy are documented in +[`docs/persona-and-distribution.md`](docs/persona-and-distribution.md). + +## Install (Recommended) + +End users do not need `uv`. + +### Debian/Ubuntu (`.deb`) + +Download a release artifact and install it: + +```bash +sudo apt install ./aman__.deb +``` + +Then enable the user service: + +```bash +systemctl --user daemon-reload +systemctl --user enable --now aman +``` + +### Arch Linux + +Use the generated packaging inputs (`PKGBUILD` + source tarball) in `dist/arch/` +or your own packaging pipeline. + +## Distribution Matrix + +| Channel | Audience | Status | +| --- | --- | --- | +| Debian package (`.deb`) | End users on Ubuntu/Debian | Canonical | +| Arch `PKGBUILD` + source tarball | Arch maintainers/power users | Supported | +| Python wheel/sdist | Developers/integrators | Supported | + +## Runtime Dependencies - X11 -- `sounddevice` (PortAudio) -- `faster-whisper` -- `llama-cpp-python` -- Tray icon deps: `gtk3`, `libayatana-appindicator3` -- Python deps (core): `numpy`, `pillow`, `faster-whisper`, `llama-cpp-python`, `sounddevice` -- X11 extras: `PyGObject`, `python-xlib` - -System packages (example names): `portaudio`/`libportaudio2`. +- PortAudio runtime (`libportaudio2` or distro equivalent) +- GTK3 and AppIndicator runtime (`gtk3`, `libayatana-appindicator3`) +- Python GTK and X11 bindings (`python3-gi`/`python-gobject`, `python-xlib`)
Ubuntu/Debian ```bash -sudo apt install -y portaudio19-dev libportaudio2 python3-gi gir1.2-gtk-3.0 libayatana-appindicator3-1 +sudo apt install -y libportaudio2 python3-gi python3-xlib gir1.2-gtk-3.0 libayatana-appindicator3-1 ```
@@ -28,7 +66,7 @@ sudo apt install -y portaudio19-dev libportaudio2 python3-gi gir1.2-gtk-3.0 liba Arch Linux ```bash -sudo pacman -S --needed portaudio gtk3 libayatana-appindicator +sudo pacman -S --needed portaudio gtk3 libayatana-appindicator python-gobject python-xlib ``` @@ -37,7 +75,7 @@ sudo pacman -S --needed portaudio gtk3 libayatana-appindicator Fedora ```bash -sudo dnf install -y portaudio portaudio-devel gtk3 libayatana-appindicator-gtk3 +sudo dnf install -y portaudio gtk3 libayatana-appindicator-gtk3 python3-gobject python3-xlib ``` @@ -46,25 +84,15 @@ sudo dnf install -y portaudio portaudio-devel gtk3 libayatana-appindicator-gtk3 openSUSE ```bash -sudo zypper install -y portaudio portaudio-devel gtk3 libayatana-appindicator3-1 +sudo zypper install -y portaudio gtk3 libayatana-appindicator3-1 python3-gobject python3-python-xlib ``` -## Python Daemon - -Install Python deps: - -X11 (supported): - -```bash -uv sync --extra x11 -``` - ## Quickstart ```bash -uv run aman run +aman run ``` On first launch, Aman opens a graphical settings window automatically. @@ -191,15 +219,13 @@ STT hinting: ## systemd user service ```bash -uv pip install --user . -cp systemd/aman.service ~/.config/systemd/user/aman.service -systemctl --user daemon-reload -systemctl --user enable --now aman +make install-service ``` Service notes: -- The user unit launches `aman` from `PATH`; ensure `~/.local/bin` is present in your user PATH. +- The user unit launches `aman` from `PATH`. +- Package installs should provide the `aman` command automatically. - Inspect failures with `systemctl --user status aman` and `journalctl --user -u aman -f`. ## Usage @@ -228,21 +254,50 @@ AI processing: - Default local llama.cpp model. - Optional external API provider through `llm.provider=external_api`. +Build and packaging (maintainers): + +```bash +make build +make package +make package-deb +make package-arch +make release-check +``` + +`make package-deb` installs Python dependencies while creating the package. +For offline packaging, set `AMAN_WHEELHOUSE_DIR` to a directory containing the +required wheels. + Control: ```bash make run +make run config.example.json make doctor make self-check make check ``` -CLI (internal/support fallback, mostly for automation/tests): +Developer setup (optional, `uv` workflow): ```bash +uv sync --extra x11 uv run aman run --config ~/.config/aman/config.json -uv run aman doctor --config ~/.config/aman/config.json --json -uv run aman self-check --config ~/.config/aman/config.json --json -uv run aman version -uv run aman init --config ~/.config/aman/config.json --force +``` + +Developer setup (optional, `pip` workflow): + +```bash +make install-local +aman run --config ~/.config/aman/config.json +``` + +CLI (internal/support fallback): + +```bash +aman run --config ~/.config/aman/config.json +aman doctor --config ~/.config/aman/config.json --json +aman self-check --config ~/.config/aman/config.json --json +aman version +aman init --config ~/.config/aman/config.json --force ``` diff --git a/docs/persona-and-distribution.md b/docs/persona-and-distribution.md new file mode 100644 index 0000000..5cec2b5 --- /dev/null +++ b/docs/persona-and-distribution.md @@ -0,0 +1,50 @@ +# Aman Target Persona and Distribution Strategy + +## Primary Persona: Desktop Professional + +This is the canonical Aman user. + +- Uses Linux desktop daily (X11 today), mostly Ubuntu/Debian. +- Wants fast dictation and rewriting without learning Python tooling. +- Prefers GUI setup and tray usage over CLI. +- Expects normal install/uninstall/update behavior from system packages. + +Design implications: + +- End-user install path must not require `uv`. +- Runtime defaults should work with minimal input. +- Documentation should prioritize package install first. + +## Secondary Persona: Power User + +- Comfortable with CLI, package internals, and model customization. +- Uses advanced config, external APIs, or custom models. +- Can run diagnostics and debug logs when needed. + +Design implications: + +- Keep Make and Python workflows available. +- Keep explicit expert-mode knobs in settings and config. +- Keep docs for development separate from standard install docs. + +## Supported Distribution Path (Current) + +Tiered distribution model: + +1. Canonical: Debian package (`.deb`) for Ubuntu/Debian users. +2. Secondary: Arch package inputs (`PKGBUILD` + source tarball). +3. Developer: wheel/sdist from `python -m build`. + +## Out of Scope for Initial Packaging + +- Wayland production support. +- Flatpak/snap-first distribution. +- Cross-platform desktop installers outside Linux. + +## Release and Support Policy + +- App versioning follows SemVer (`0.y.z` until API/UX stabilizes). +- Config schema versioning is independent (`config_version` in config). +- Packaging docs must always separate: + - End-user install path (package-first) + - Developer setup path (uv/pip/build workflows) diff --git a/docs/release-checklist.md b/docs/release-checklist.md index f66d068..da22570 100644 --- a/docs/release-checklist.md +++ b/docs/release-checklist.md @@ -2,11 +2,16 @@ 1. Update `CHANGELOG.md` with final release notes. 2. Bump `project.version` in `pyproject.toml`. -3. Run: - - `python3 -m py_compile src/*.py tests/*.py` - - `python3 -m unittest discover -s tests -p 'test_*.py'` -4. Build artifacts: - - `python3 -m build` -5. Tag release: +3. Run quality and build gates: + - `make release-check` +4. Build packaging artifacts: + - `make package` +5. Verify artifacts: + - `dist/*.whl` + - `dist/*.tar.gz` + - `dist/*.deb` + - `dist/arch/PKGBUILD` +6. Tag release: - `git tag vX.Y.Z` - `git push origin vX.Y.Z` +7. Publish release and upload package artifacts from `dist/`. diff --git a/packaging/arch/PKGBUILD.in b/packaging/arch/PKGBUILD.in new file mode 100644 index 0000000..8fc7e86 --- /dev/null +++ b/packaging/arch/PKGBUILD.in @@ -0,0 +1,33 @@ +# Maintainer: Aman Maintainers +pkgname=aman +pkgver=__VERSION__ +pkgrel=1 +pkgdesc="Local amanuensis daemon for X11 desktops" +arch=('x86_64') +url="https://github.com/example/aman" +license=('MIT') +depends=('python' 'python-pip' 'python-setuptools' 'portaudio' 'gtk3' 'libayatana-appindicator' 'python-gobject' 'python-xlib') +makedepends=('python-build' 'python-installer' 'python-wheel') +source=("__TARBALL_NAME__") +sha256sums=('__TARBALL_SHA256__') + +prepare() { + cd "${srcdir}/aman-${pkgver}" + python -m build --wheel +} + +package() { + cd "${srcdir}/aman-${pkgver}" + install -dm755 "${pkgdir}/opt/aman" + python -m venv --system-site-packages "${pkgdir}/opt/aman/venv" + "${pkgdir}/opt/aman/venv/bin/python" -m pip install --upgrade pip + "${pkgdir}/opt/aman/venv/bin/python" -m pip install "dist/aman-${pkgver}-"*.whl + + install -Dm755 /dev/stdin "${pkgdir}/usr/bin/aman" <<'EOF' +#!/usr/bin/env bash +set -euo pipefail +exec /opt/aman/venv/bin/aman "$@" +EOF + + install -Dm644 "systemd/aman.service" "${pkgdir}/usr/lib/systemd/user/aman.service" +} diff --git a/packaging/deb/control.in b/packaging/deb/control.in new file mode 100644 index 0000000..74a906d --- /dev/null +++ b/packaging/deb/control.in @@ -0,0 +1,11 @@ +Package: __PACKAGE_NAME__ +Version: __VERSION__ +Section: utils +Priority: optional +Architecture: __ARCH__ +Maintainer: Aman Maintainers +Depends: python3, python3-venv, python3-gi, python3-xlib, libportaudio2, gir1.2-gtk-3.0, libayatana-appindicator3-1 +Description: Aman local amanuensis daemon for X11 desktops + Aman records microphone input, transcribes speech, optionally rewrites output, + and injects text into the focused desktop app. Includes tray controls and a + first-run graphical settings UI. diff --git a/packaging/deb/postinst b/packaging/deb/postinst new file mode 100755 index 0000000..4094fb4 --- /dev/null +++ b/packaging/deb/postinst @@ -0,0 +1,10 @@ +#!/bin/sh +set -eu + +cat <<'EOF' +Aman installed. + +To enable auto-start for your user session: + systemctl --user daemon-reload + systemctl --user enable --now aman +EOF diff --git a/scripts/package_arch.sh b/scripts/package_arch.sh new file mode 100755 index 0000000..9e2349e --- /dev/null +++ b/scripts/package_arch.sh @@ -0,0 +1,44 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +ROOT_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)" +source "${SCRIPT_DIR}/package_common.sh" + +require_command git +require_command sha256sum + +VERSION="$(project_version)" +PACKAGE_NAME="$(project_name)" +ARCH_DIST_DIR="${DIST_DIR}/arch" +TARBALL_NAME="${PACKAGE_NAME}-${VERSION}.tar.gz" +TARBALL_PATH="${ARCH_DIST_DIR}/${TARBALL_NAME}" +PKGBUILD_PATH="${ARCH_DIST_DIR}/PKGBUILD" + +mkdir -p "${ARCH_DIST_DIR}" + +git -C "${ROOT_DIR}" archive \ + --format=tar.gz \ + --prefix="${PACKAGE_NAME}-${VERSION}/" \ + HEAD \ + > "${TARBALL_PATH}" + +TARBALL_SHA256="$(sha256sum "${TARBALL_PATH}" | awk '{print $1}')" + +render_template \ + "${ROOT_DIR}/packaging/arch/PKGBUILD.in" \ + "${PKGBUILD_PATH}" \ + "VERSION=${VERSION}" \ + "TARBALL_NAME=${TARBALL_NAME}" \ + "TARBALL_SHA256=${TARBALL_SHA256}" + +echo "generated ${PKGBUILD_PATH}" +echo "generated ${TARBALL_PATH}" + +if [[ "${BUILD_ARCH_PACKAGE:-0}" == "1" ]]; then + require_command makepkg + ( + cd "${ARCH_DIST_DIR}" + makepkg -f + ) +fi diff --git a/scripts/package_common.sh b/scripts/package_common.sh new file mode 100755 index 0000000..57b1a59 --- /dev/null +++ b/scripts/package_common.sh @@ -0,0 +1,84 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +ROOT_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)" +DIST_DIR="${ROOT_DIR}/dist" +BUILD_DIR="${ROOT_DIR}/build" +APP_NAME="aman" + +mkdir -p "${DIST_DIR}" "${BUILD_DIR}" + +require_command() { + local cmd="$1" + if command -v "${cmd}" >/dev/null 2>&1; then + return + fi + echo "missing required command: ${cmd}" >&2 + exit 1 +} + +project_version() { + require_command python3 + python3 - <<'PY' +from pathlib import Path +import re + +text = Path("pyproject.toml").read_text(encoding="utf-8") +match = re.search(r'(?m)^version\s*=\s*"([^"]+)"\s*$', text) +if not match: + raise SystemExit("project version not found in pyproject.toml") +print(match.group(1)) +PY +} + +project_name() { + require_command python3 + python3 - <<'PY' +from pathlib import Path +import re + +text = Path("pyproject.toml").read_text(encoding="utf-8") +match = re.search(r'(?m)^name\s*=\s*"([^"]+)"\s*$', text) +if not match: + raise SystemExit("project name not found in pyproject.toml") +print(match.group(1)) +PY +} + +build_wheel() { + require_command python3 + python3 -m build --wheel --no-isolation +} + +latest_wheel_path() { + require_command python3 + python3 - <<'PY' +from pathlib import Path +import re + +text = Path("pyproject.toml").read_text(encoding="utf-8") +name_match = re.search(r'(?m)^name\s*=\s*"([^"]+)"\s*$', text) +version_match = re.search(r'(?m)^version\s*=\s*"([^"]+)"\s*$', text) +if not name_match or not version_match: + raise SystemExit("project metadata not found in pyproject.toml") +name = name_match.group(1).replace("-", "_") +version = version_match.group(1) +candidates = sorted(Path("dist").glob(f"{name}-{version}-*.whl")) +if not candidates: + raise SystemExit("no wheel artifact found in dist/") +print(candidates[-1]) +PY +} + +render_template() { + local template_path="$1" + local output_path="$2" + shift 2 + cp "${template_path}" "${output_path}" + for mapping in "$@"; do + local key="${mapping%%=*}" + local value="${mapping#*=}" + sed -i "s|__${key}__|${value}|g" "${output_path}" + done +} diff --git a/scripts/package_deb.sh b/scripts/package_deb.sh new file mode 100755 index 0000000..3ed7f33 --- /dev/null +++ b/scripts/package_deb.sh @@ -0,0 +1,64 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +ROOT_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)" +source "${SCRIPT_DIR}/package_common.sh" + +require_command dpkg-deb +require_command python3 + +VERSION="$(project_version)" +PACKAGE_NAME="$(project_name)" +ARCH="${DEB_ARCH:-}" +if [[ -z "${ARCH}" ]]; then + if command -v dpkg >/dev/null 2>&1; then + ARCH="$(dpkg --print-architecture)" + else + ARCH="amd64" + fi +fi + +build_wheel +WHEEL_PATH="$(latest_wheel_path)" + +STAGE_DIR="${BUILD_DIR}/deb/${PACKAGE_NAME}_${VERSION}_${ARCH}" +PACKAGE_BASENAME="${PACKAGE_NAME}_${VERSION}_${ARCH}" +DEB_PATH="${DIST_DIR}/${PACKAGE_BASENAME}.deb" +VENV_DIR="${STAGE_DIR}/opt/${PACKAGE_NAME}/venv" +PIP_ARGS=() +if [[ -n "${AMAN_WHEELHOUSE_DIR:-}" ]]; then + PIP_ARGS+=(--no-index --find-links "${AMAN_WHEELHOUSE_DIR}") +fi + +rm -rf "${STAGE_DIR}" +mkdir -p "${STAGE_DIR}/DEBIAN" +mkdir -p "${STAGE_DIR}/usr/bin" +mkdir -p "${STAGE_DIR}/usr/lib/systemd/user" +mkdir -p "${STAGE_DIR}/opt/${PACKAGE_NAME}" + +render_template \ + "${ROOT_DIR}/packaging/deb/control.in" \ + "${STAGE_DIR}/DEBIAN/control" \ + "PACKAGE_NAME=${PACKAGE_NAME}" \ + "VERSION=${VERSION}" \ + "ARCH=${ARCH}" + +cp "${ROOT_DIR}/packaging/deb/postinst" "${STAGE_DIR}/DEBIAN/postinst" +chmod 0755 "${STAGE_DIR}/DEBIAN/postinst" + +python3 -m venv --system-site-packages "${VENV_DIR}" +"${VENV_DIR}/bin/python" -m pip install "${PIP_ARGS[@]}" "${WHEEL_PATH}" + +cat >"${STAGE_DIR}/usr/bin/${PACKAGE_NAME}" <