Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
1ba0676
Design: tag-derived versioning via hatch-vcs
alexkroman-assembly Jun 12, 2026
556a08a
Design: switch to hatch-vcs version-file approach
alexkroman-assembly Jun 12, 2026
58dc0de
Plan: tag-derived versioning via hatch-vcs
alexkroman-assembly Jun 12, 2026
e98376e
wip: dynamic vcs version
alexkroman-assembly Jun 12, 2026
942b087
build: exclude generated _version.py from lint/dead-code gates
alexkroman-assembly Jun 12, 2026
7ec7201
feat: source __version__ from hatch-vcs _version.py
alexkroman-assembly Jun 12, 2026
d1d5a5f
build(brew): pass tag version to hatch-vcs (tarball has no .git)
alexkroman-assembly Jun 12, 2026
a620358
build(brew): scope pretend-version to our package, not resources
alexkroman-assembly Jun 12, 2026
f46e055
build: cut_release derives version from tags; drop bump_patch.sh
alexkroman-assembly Jun 12, 2026
5960e7e
docs: document tag-derived versioning in CLAUDE.md
alexkroman-assembly Jun 12, 2026
08110c8
build: drop deleted bump_patch.sh from check.sh shellcheck list
alexkroman-assembly Jun 12, 2026
9ed1a79
build: allowlist gitleaks false positive on stream_exec kwarg expression
alexkroman-assembly Jun 12, 2026
95ff908
feat: drop tagline from welcome banner, keep full VCS version
alexkroman-assembly Jun 12, 2026
b8ffd5d
docs: add welcome screenshot + 'Things you can do' showcase to README
alexkroman-assembly Jun 12, 2026
d4da330
fix: cut_release explicit version no longer requires an existing tag
alexkroman-assembly Jun 12, 2026
fe7a527
Merge remote-tracking branch 'origin/main' into hatch-vcs-versioning
alexkroman-assembly Jun 12, 2026
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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ transcribe/
# Wrong tool for this project (hatchling + uv); never commit a poetry lock
poetry.lock

# Generated by hatch-vcs at build time (version derived from the git tag)
aai_cli/_version.py

# Brainstorming visual-companion scratch
.superpowers/

Expand Down
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ the return value comes from a recorded payload instead of a hand-built mock.
- The **package/module** is `aai_cli`; the **distribution** name is `aai-cli`; the **console command** is `assembly` (`[project.scripts] assembly = "aai_cli.main:run"`).
- `assembly init` templates live in `aai_cli/init/templates/` and are **committed**, including renamed dotfiles (`gitignore` → `.gitignore`, `env.example`). The wheel force-includes them via `[tool.hatch.build.targets.wheel] artifacts`, excluding `__pycache__/*.pyc`. Editing templates needs care — see the parametrized contract tests (`tests/test_init_template_*.py`).
- `audioop` left the stdlib in 3.13; `audioop-lts` backfills it (conditional dependency). Supported Pythons: 3.12–3.13.
- **Releasing is tag-triggered.** `.github/workflows/release.yml` fires on a pushed `vX.Y.Z` tag and builds the prebuilt arm64 Homebrew bottle (`Formula/assembly.rb`), cuts the GitHub Release, and opens the formula PR — bottling matters because the deps include Rust-backed sdists (`pydantic-core`, `jiter`, `cryptography`) that would otherwise compile from source on `brew install`. Two committed helpers drive it and are self-documenting (`--help`): `scripts/bump_patch.sh` rewrites the version in lock-step across `pyproject.toml` + `aai_cli/__init__.py` (run on a branch → merge the PR), then `scripts/cut_release.sh` tags + pushes. **`cut_release.sh` only runs from a clean `main` in sync with `origin/main`** (it hard-errors on a feature branch / dirty tree / version mismatch), so cut releases from `main`, not your working branch. The "update available" notice users see is `aai_cli/update_check.py`.
- **Releasing is tag-triggered.** The version is **derived from the git tag** by hatch-vcs and written to a gitignored `aai_cli/_version.py` at build time — there is no version string to keep in sync across `pyproject.toml` or `aai_cli/__init__.py`, and `bump_patch.sh` no longer exists. To cut a release, run `scripts/cut_release.sh` from a clean `main` in sync with `origin/main`: no argument → next patch above the latest `vX.Y.Z` tag; `cut_release.sh X.Y.Z` → explicit version. It tags + pushes, which fires `.github/workflows/release.yml` — that builds the prebuilt arm64 Homebrew bottle (`Formula/assembly.rb`), cuts the GitHub Release, and opens the formula PR. Bottling matters because the deps include Rust-backed sdists (`pydantic-core`, `jiter`, `cryptography`) that would otherwise compile from source on `brew install`. The Homebrew formula builds from a git-less GitHub source tarball, so `Formula/assembly.rb`'s `def install` sets the generic `SETUPTOOLS_SCM_PRETEND_VERSION` env var (installing resources first under a clean env, then setting the var for our package only) to feed the tag version to the build. **`cut_release.sh` only runs from a clean `main` in sync with `origin/main`** (it hard-errors on a feature branch / dirty tree), so cut releases from `main`, not your working branch. The "update available" notice users see is `aai_cli/update_check.py`.

## Architecture

Expand Down
13 changes: 12 additions & 1 deletion Formula/assembly.rb
Original file line number Diff line number Diff line change
Expand Up @@ -288,7 +288,18 @@ class Assembly < Formula
end

def install
virtualenv_install_with_resources
# The GitHub source tarball has no .git, so hatch-vcs cannot derive the
# version at build time. hatch-vcs (0.x) does not forward dist_name to
# setuptools-scm, so only the GENERIC SETUPTOOLS_SCM_PRETEND_VERSION is
# honored — but a global generic pretend-version would also override the
# version of any *resource* that builds via setuptools-scm. So install the
# pinned resources first under a clean env, then set the pretend-version and
# install only our own package (the build hook writes the tag version into
# the installed aai_cli/_version.py).
venv = virtualenv_create(libexec, "python3.13")
venv.pip_install resources
ENV["SETUPTOOLS_SCM_PRETEND_VERSION"] = version.to_s
venv.pip_install_and_link buildpath
end

test do
Expand Down
73 changes: 73 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@

The AssemblyAI CLI (`assembly`) brings speech AI to your terminal: transcribe files, URLs, and YouTube/podcast pages, stream live audio, talk to a two-way voice agent, prompt the LLM Gateway, benchmark speech models, and scaffold ready-to-deploy starter apps.

<p align="center">
<img src="assets/welcome.png" alt="The assembly CLI welcome screen, listing command groups for transcription, streaming, voice agents, app scaffolding, and account management" width="820">
</p>

## 🚀 Why the AssemblyAI CLI?

- **🎯 One command for everything**: transcription, real-time streaming, voice agents, LLM prompts, and WER benchmarking — no SDK boilerplate.
Expand All @@ -15,6 +19,75 @@ The AssemblyAI CLI (`assembly`) brings speech AI to your terminal: transcribe fi
- **🤖 Agent-ready**: `assembly setup install` wires your coding agent up with the AssemblyAI docs MCP server and skills.
- **📖 Open source**: MIT licensed.

## ✨ Things you can do with it

A few one-liners that show what `assembly` can do. These are the fun ones; the everyday basics live under **Quick examples** below.

**Recreate a scene with synthetic voices** — transcribe and diarize a YouTube clip, then pipe it straight into TTS with a different voice per speaker:

```sh
assembly transcribe "https://www.youtube.com/watch?v=awmCtXzFsJo" --speaker-labels \
| assembly --sandbox speak --voice A=jane --voice B=mary --out scene.wav
```

`speak` auto-detects `Speaker A:` labels, merges each speaker's turns, and rotates voices. (`speak` is sandbox-only today, hence `--sandbox`.)

**Turn a podcast into audio** — Apple and Spotify podcast pages work too (yt-dlp ingestion):

```sh
assembly transcribe "https://podcasts.apple.com/us/podcast/id1516093381" --speaker-labels \
| assembly --sandbox speak --out episode.wav
```

**Keep a live to-do list from your mic** — `llm -f` re-runs the prompt over the growing transcript, updating in place:

```sh
assembly stream -o text | assembly llm -f "summarize my to-dos as I talk"
```

**Caption a meeting from system audio** (macOS) — captures app/system audio alongside your mic as separate diarized speakers:

```sh
assembly stream --system-audio --speaker-labels -o text
```

**Get pinged when your name comes up** in a live meeting:

```sh
assembly stream -o text | grep --line-buffered -i alex \
| while read -r _; do afplay /System/Library/Sounds/Glass.aiff; done
```

**Chain LLM prompts over a transcript** — each prompt runs on the finished transcript:

```sh
assembly transcribe --sample --llm "summarize" --llm "translate the summary to French"
```

**Talk to a voice agent in your terminal** — full-duplex, around 20 voices:

```sh
assembly agent --voice ivy --system-prompt "you're a helpful interviewer"
```

**Graduate to the SDK** — `--show-code` prints the equivalent Python script for any `transcribe`/`stream`/`agent` run instead of executing it:

```sh
assembly agent --system-prompt "you're a story generator" --show-code > story.py
```

**Scaffold and deploy a voice agent** — templates: `voice-agent`, `audio-transcription`, `live-captions`:

```sh
assembly init voice-agent && assembly deploy --prod
```

**Benchmark WER against public datasets** — built-in aliases for LibriSpeech, TEDLIUM, and more:

```sh
assembly eval librispeech --speech-model universal-3-pro --limit 50
```

## 📦 Installation

Requires Python 3.12+ (Homebrew brings its own; for pipx/uv see the `--python` hint below).
Expand Down
4 changes: 3 additions & 1 deletion aai_cli/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
__version__ = "0.1.4"
from aai_cli._version import __version__

__all__ = ["__version__"]
4 changes: 2 additions & 2 deletions aai_cli/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -252,8 +252,8 @@ def _format_click_error_fixed(self: ClickException) -> None:

app = typer.Typer(
name="assembly",
# No top-level `help=`: the bare-`assembly` welcome banner already carries the
# "AssemblyAI from your terminal" tagline, so a description here would duplicate it.
# No top-level `help=`: the bare-`assembly` welcome banner plus the command table
# below already introduce the tool, so a description here would be redundant.
# `assembly --install-completion` / `--show-completion` for bash/zsh/fish/PowerShell,
# the discoverability affordance gh/kubectl/docker users reach for.
add_completion=True,
Expand Down
14 changes: 6 additions & 8 deletions aai_cli/output.py
Original file line number Diff line number Diff line change
Expand Up @@ -225,18 +225,16 @@ def emit_error(err: CLIError, *, json_mode: bool) -> None:
error_console.print(f"[aai.muted]Suggestion:[/aai.muted] {escape(err.suggestion)}")


# A one-line header: emoji + product + version, then the product tagline.
_TAGLINE = "AssemblyAI from your terminal"
# A one-line header: emoji + product + version.


def print_banner() -> None:
"""Print the welcome header — a single emoji + product + version + tagline line
in the brand accent (the bare-command welcome screen)."""
# highlight=False so Rich's repr-highlighter doesn't recolor the version digits or
# the quoted tagline — the line stays a single muted tone behind the brand label.
"""Print the welcome header — a single emoji + product + version line in the
brand accent (the bare-command welcome screen)."""
# highlight=False so Rich's repr-highlighter doesn't recolor the version digits
# the line stays a single muted tone behind the brand label.
console.print(
f"[aai.brand]🎙️ AssemblyAI CLI[/aai.brand] "
f"[aai.muted]{__version__} — {_TAGLINE}[/aai.muted]",
f"[aai.brand]🎙️ AssemblyAI CLI[/aai.brand] [aai.muted]{__version__}[/aai.muted]",
highlight=False, # pragma: no mutate (purely cosmetic: toggles Rich repr coloring, not text)
)

Expand Down
Binary file added assets/welcome.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading