From b643c2ce659954151d9e092afb67de5693ab74d0 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 16 Jun 2026 21:13:57 +0000 Subject: [PATCH] Add -o/--output to dictate for parity with stream/agent dictate only exposed --json, so `assembly dictate -o text` errored even though plain transcript text is already the non-JSON default. Add the -o/--output {text,json} flag mirroring stream/agent: -o json folds into json_mode (== --json), -o text is the explicit bare-transcript default, and --json + -o text is rejected as a contradictory pair via the shared resolve_output_modes helper. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01Knhve9pgqPSg2kQhxnV7Wn --- aai_cli/commands/dictate/__init__.py | 9 ++++++ aai_cli/commands/dictate/_exec.py | 10 +++++- .../test_snapshots_help_run.ambr | 6 ++++ tests/test_dictate_exec.py | 32 +++++++++++++++++-- 4 files changed, 54 insertions(+), 3 deletions(-) diff --git a/aai_cli/commands/dictate/__init__.py b/aai_cli/commands/dictate/__init__.py index 24e5d7d7..c763f1ad 100644 --- a/aai_cli/commands/dictate/__init__.py +++ b/aai_cli/commands/dictate/__init__.py @@ -5,6 +5,7 @@ from aai_cli import command_registry, help_panels, options from aai_cli.app.context import run_with_options from aai_cli.commands.dictate import _exec as dictate_exec +from aai_cli.core import choices from aai_cli.core.sync_stt import MAX_AUDIO_SECONDS from aai_cli.ui.help_text import examples_epilog @@ -29,6 +30,7 @@ "assembly dictate --word-boost AssemblyAI --word-boost LeMUR", ), ("One JSON object per utterance", "assembly dictate --json"), + ("Pipe the bare transcript onward", "assembly dictate -o text | assembly llm -f"), ] ), ) @@ -58,6 +60,12 @@ def dictate( max=float(MAX_AUDIO_SECONDS), ), json_out: bool = options.json_option("Emit one JSON object per utterance"), + output_field: choices.TextOrJson | None = typer.Option( + None, + "-o", + "--output", + help="Output mode: text (the bare transcript per utterance, pipe-friendly) or json", + ), ) -> None: """Push-to-talk dictation: record the mic, get the transcript back @@ -73,5 +81,6 @@ def dictate( device=device, once=once, max_seconds=max_seconds, + output_field=output_field, ) run_with_options(ctx, dictate_exec.run_dictate, opts, json=json_out) diff --git a/aai_cli/commands/dictate/_exec.py b/aai_cli/commands/dictate/_exec.py index d2a07d77..371c2ee1 100644 --- a/aai_cli/commands/dictate/_exec.py +++ b/aai_cli/commands/dictate/_exec.py @@ -13,10 +13,11 @@ from dataclasses import dataclass from aai_cli.app.context import AppState -from aai_cli.core import sync_stt +from aai_cli.core import choices, sync_stt from aai_cli.core.config_builder import split_csv from aai_cli.core.hotkey import CTRL_C, CTRL_D, ESC, TerminalKeys from aai_cli.core.microphone import MicrophoneSource +from aai_cli.streaming.session import resolve_output_modes from aai_cli.ui import output # Capture is resampled to one rate the Sync API accepts; 16 kHz mono PCM16 keeps @@ -41,6 +42,8 @@ class DictateOptions: device: int | None once: bool max_seconds: float + # -o/--output: text (the default bare-transcript shape) or json (== --json). + output_field: choices.TextOrJson | None = None def _note(message: str, *, json_mode: bool, quiet: bool) -> None: @@ -165,6 +168,11 @@ def _session( def run_dictate(opts: DictateOptions, state: AppState, *, json_mode: bool) -> None: """Execute one `assembly dictate` invocation from already-parsed flags.""" + # Fold -o/--output into json_mode (-o json == --json) and reject the + # contradictory --json + -o text pair, the same way `stream`/`agent` do. + # dictate has no live panel, so the text_mode half is unused — plain + # transcript text is already the non-JSON default in `_emit`. + _, json_mode = resolve_output_modes(opts.output_field, json_mode=json_mode) try: # Entering TerminalKeys validates the terminal (a usage precondition) # before credentials, so a piped stdin reads as "needs a terminal" — not diff --git a/tests/__snapshots__/test_snapshots_help_run.ambr b/tests/__snapshots__/test_snapshots_help_run.ambr index 5b19f1df..6ef8d171 100644 --- a/tests/__snapshots__/test_snapshots_help_run.ambr +++ b/tests/__snapshots__/test_snapshots_help_run.ambr @@ -373,6 +373,10 @@ │ [default: 120.0] │ │ --json -j Emit one JSON object per │ │ utterance │ + │ --output -o [text|json] Output mode: text (the │ + │ bare transcript per │ + │ utterance, pipe-friendly) │ + │ or json │ │ --help Show this message and │ │ exit. │ ╰──────────────────────────────────────────────────────────────────────────────╯ @@ -388,6 +392,8 @@ $ assembly dictate --word-boost AssemblyAI --word-boost LeMUR One JSON object per utterance $ assembly dictate --json + Pipe the bare transcript onward + $ assembly dictate -o text | assembly llm -f diff --git a/tests/test_dictate_exec.py b/tests/test_dictate_exec.py index b8174840..08af8245 100644 --- a/tests/test_dictate_exec.py +++ b/tests/test_dictate_exec.py @@ -16,8 +16,8 @@ from aai_cli.app.context import AppState from aai_cli.commands.dictate import _exec as dictate_exec -from aai_cli.core import config, sync_stt -from aai_cli.core.errors import CLIError +from aai_cli.core import choices, config, sync_stt +from aai_cli.core.errors import CLIError, UsageError DICTATE_DEFAULTS = dictate_exec.DictateOptions( language=None, @@ -145,6 +145,34 @@ def test_json_mode_emits_one_ndjson_object_per_utterance(seams, capsys): assert captured.err == "" +def test_output_json_folds_into_ndjson_without_the_json_flag(seams, capsys): + # -o json must enable NDJSON on its own (json_mode stays the --json flag, + # which is False here) — proving the -o/--output resolution runs. + seams["keys"] = FakeKeys(["\r", "\r"]) + _run(dataclasses.replace(DICTATE_DEFAULTS, output_field=choices.TextOrJson.json)) + assert json.loads(capsys.readouterr().out)["text"] == "hello world" + + +def test_output_text_emits_bare_transcript(seams, capsys): + # -o text is the explicit spelling of the human default: bare text, no JSON. + seams["keys"] = FakeKeys(["\r", "\r"]) + _run(dataclasses.replace(DICTATE_DEFAULTS, output_field=choices.TextOrJson.text)) + out = capsys.readouterr().out + assert out.strip() == "hello world" + assert "{" not in out + + +def test_output_text_conflicts_with_json_flag(seams): + # --json + -o text are contradictory output shapes: a clean usage error, + # the same as `stream`/`agent`. + seams["keys"] = FakeKeys(["\r", "\r"]) + with pytest.raises(UsageError): + _run( + dataclasses.replace(DICTATE_DEFAULTS, output_field=choices.TextOrJson.text), + json_mode=True, + ) + + def test_quiet_suppresses_the_interactive_hints(seams, capsys): seams["keys"] = FakeKeys(["\r", "\r"]) _run(state=AppState(quiet=True))