Skip to content
Merged
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
2 changes: 2 additions & 0 deletions .importlinter
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ source_modules =
aai_cli.streaming
aai_cli.telemetry
aai_cli.theme
aai_cli.transcribe_batch
aai_cli.transcribe_exec
aai_cli.transcribe_render
aai_cli.ws
aai_cli.youtube
Expand Down
15 changes: 14 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ Your key is written to a git-ignored `.env` (never sent to the browser). Use `--
| --- | --- |
| `assembly login` / `logout` / `whoami` | Manage the stored API key. |
| `assembly doctor` | Check your environment (API key, network, ffmpeg, microphone, agent tooling). |
| `assembly transcribe <file\|url>` | Transcribe a file, URL, or YouTube/podcast page URL (`--sample`, `--llm`, `--show-code`). |
| `assembly transcribe <file\|url>` | Transcribe a file, URL, or YouTube/podcast page URL (`--sample`, `--llm`, `--show-code`) — or a directory/glob/stdin list as a resumable batch. |
| `assembly transcripts list` / `get <id>` | Browse and fetch past transcripts. |
| `assembly stream [file]` | Real-time transcription from a file or the microphone. |
| `assembly agent` | *Run* a live two-way voice conversation (to **build** a voice agent app, use `assembly init voice-agent`). |
Expand Down Expand Up @@ -225,6 +225,19 @@ done

A Ctrl-C in a pipe hits both sides; to stop just the producer and let the consumer finish, signal the producer (`timeout -s INT 30s assembly stream …`) or end on a natural pause (`assembly stream --inactivity-timeout 5`).

## Batch transcription

Point `assembly transcribe` at a directory or glob — or pipe a list of paths/URLs with `--from-stdin` — and it transcribes everything concurrently behind a live progress table:

```sh
assembly transcribe ./recordings # every audio file under the directory
assembly transcribe "calls/*.mp3" # glob (quote it so your shell doesn't expand it)
find . -name "*.wav" | assembly transcribe --from-stdin # composes with find/ls/yt-dlp
assembly transcribe ./recordings --concurrency 8 # default is 4 at a time
```

Each source gets a `<name>.aai.json` sidecar with the full transcript payload. The sidecar is also the resume marker: a re-run skips any source whose sidecar records a completed transcription of the same bytes (hash-checked for local files), so retrying a partly-failed batch only pays for what's missing. `--force` re-transcribes everything. Under `--json`, batch mode emits one NDJSON record per source as it finishes (`{source, status, id, sidecar}`, or `{source, status, error}` on failure); the exit code is `1` if any source failed.

## API Key & Security

`assembly` resolves your key in order: the `ASSEMBLYAI_API_KEY` environment variable, then the OS keyring (written only by `assembly login`). Two things worth knowing:
Expand Down
33 changes: 31 additions & 2 deletions aai_cli/commands/transcribe.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
llm,
options,
output,
transcribe_batch,
transcribe_exec,
)

Expand All @@ -32,6 +33,8 @@
epilog=examples_epilog(
[
("Transcribe a local file", "assembly transcribe call.mp3"),
("Batch-transcribe a folder", "assembly transcribe ./recordings"),
("Batch-transcribe a glob", 'assembly transcribe "calls/*.mp3"'),
("Try it with the hosted sample", "assembly transcribe --sample"),
("Transcribe a YouTube video", "assembly transcribe https://youtu.be/dtp6b76pMak"),
("Transcribe a podcast page", 'assembly transcribe "https://podcasts.apple.com/…"'),
Expand All @@ -44,8 +47,14 @@
)
def transcribe(
ctx: typer.Context,
source: str | None = typer.Argument(None, help="Audio file, URL, or YouTube/podcast URL."),
source: str | None = typer.Argument(
None, help="Audio file, URL, YouTube/podcast URL, or a directory/glob (batch mode)."
),
sample: bool = typer.Option(False, "--sample", help="Use the hosted wildfires.mp3 sample."),
# batch mode
from_stdin: bool = options.batch_from_stdin_option(),
concurrency: int = options.batch_concurrency_option(),
force: bool = options.batch_force_option(),
# model & language
speech_model: aai.SpeechModel | None = typer.Option(
None,
Expand Down Expand Up @@ -334,13 +343,17 @@ def transcribe(
help="Print the equivalent Python SDK code and exit (does not transcribe).",
),
) -> None:
"""Transcribe an audio file, URL, or YouTube/podcast link.
"""Transcribe an audio file, URL, or YouTube/podcast link — or a whole batch.

Quickest start: assembly transcribe call.mp3 (or --sample for the hosted demo).

Save with --out FILE, or pipe one field with -o text. YouTube and podcast-page
URLs (any page yt-dlp can extract) are downloaded first, then transcribed.

Batch mode: pass a directory or glob (or pipe a list with --from-stdin) to
transcribe many sources concurrently. Each source gets a .aai.json sidecar
with the full result, and a re-run skips sources already transcribed.

Curated flags cover common features; --config KEY=VALUE and --config-file reach
every other field. Analysis (summary, chapters, ...) renders in human mode.
"""
Expand Down Expand Up @@ -403,6 +416,22 @@ def body(state: AppState, json_mode: bool) -> None:

transcribe_exec.validate_speakers_expected(merged)

sources = transcribe_batch.expand_sources(source, from_stdin=from_stdin, sample=sample)
if sources is not None:
transcribe_batch.reject_single_source_flags(
out=out, output_field=output_field, llm_prompt=llm_prompt, show_code=show_code
)
transcribe_batch.run_batch(
config.resolve_api_key(profile=state.profile),
sources,
transcription_config=config_builder.construct_transcription_config(merged),
concurrency=concurrency,
force=force,
json_mode=json_mode,
quiet=state.quiet,
)
return

if show_code:
# Print-only: build the equivalent script and exit without transcribing or
# authenticating (raw stdout, so `--show-code > script.py` runs). No
Expand Down
1 change: 1 addition & 0 deletions aai_cli/help_panels.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
OPT_TRANSLATION = "Translation"
OPT_ADVANCED = "Advanced"
OPT_LLM = "LLM Transform"
OPT_BATCH = "Batch" # many-source mode: --from-stdin, --concurrency, --force
# stream-specific panels (real-time concerns that file transcription has no equivalent for)
OPT_CAPTURE = "Audio Capture"
OPT_TURNS = "Turn Detection"
Expand Down
43 changes: 43 additions & 0 deletions aai_cli/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,51 @@

import typer

from aai_cli import help_panels

DEFAULT_BATCH_CONCURRENCY = 4


def json_option(help_text: str = "Output raw JSON.") -> bool:
"""The standard ``--json``/``-j`` flag; pass ``help_text`` where the shape differs."""
flag: bool = typer.Option(False, "--json", "-j", help=help_text)
return flag


# Batch-mode flags for `transcribe` (see transcribe_batch.py). Defined here because
# this module owns the FBT003 carve-out for Typer's boolean positional defaults.


def batch_from_stdin_option() -> bool:
"""The ``--from-stdin`` flag: batch mode fed one path/URL per stdin line."""
flag: bool = typer.Option(
False,
"--from-stdin",
help="Batch mode: read audio paths/URLs from stdin, one per line "
"(composes with find/ls/yt-dlp output).",
rich_help_panel=help_panels.OPT_BATCH,
)
return flag


def batch_concurrency_option() -> int:
"""The ``--concurrency`` option: how many sources transcribe at once in batch mode."""
value: int = typer.Option(
DEFAULT_BATCH_CONCURRENCY,
"--concurrency",
min=1,
help="How many sources to transcribe at once in batch mode.",
rich_help_panel=help_panels.OPT_BATCH,
)
return value


def batch_force_option() -> bool:
"""The ``--force`` flag: re-transcribe even when a completed sidecar exists."""
flag: bool = typer.Option(
False,
"--force",
help="Batch mode: re-transcribe sources whose sidecar already records a completed run.",
rich_help_panel=help_panels.OPT_BATCH,
)
return flag
Loading
Loading