Skip to content

Commit 1b90c98

Browse files
alexkromanclaude
andauthored
Add -o/--output flag to dictate command for text/json modes (#192)
Adds an `-o`/`--output` option to the `assembly dictate` command that allows users to explicitly choose between `text` (bare transcript, pipe-friendly) and `json` (one JSON object per utterance) output modes. ## Summary This change introduces output mode selection to the dictate command, bringing it into alignment with the `stream` and `agent` commands' output handling patterns. The new flag provides an explicit, discoverable way to select output format while maintaining backward compatibility (text remains the default). ## Key Changes - **New `output_field` parameter** in `DictateOptions` to capture the `-o`/`--output` choice - **Output mode resolution** via `resolve_output_modes()` in `run_dictate()` to: - Fold `-o json` into `json_mode` (equivalent to `--json`) - Reject the contradictory `--json + -o text` combination with a `UsageError` - **CLI integration** in `dictate/__init__.py`: - Added `choices.TextOrJson` enum option - Wired the parameter through to `DictateOptions` - Updated help text and examples to show the new usage pattern - **Test coverage** for all three scenarios: - `-o json` enables NDJSON output without the `--json` flag - `-o text` emits bare transcript (no JSON wrapper) - `--json + -o text` raises `UsageError` as a usage conflict ## Implementation Details The implementation reuses the existing `resolve_output_modes()` utility from the streaming session module, ensuring consistent conflict detection and resolution across commands. The dictate command's `text_mode` half of the resolution is unused (plain text is already the non-JSON default in `_emit()`), but the infrastructure is in place for future enhancements. https://claude.ai/code/session_01Knhve9pgqPSg2kQhxnV7Wn Co-authored-by: Claude <noreply@anthropic.com>
1 parent 162720c commit 1b90c98

4 files changed

Lines changed: 54 additions & 3 deletions

File tree

aai_cli/commands/dictate/__init__.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from aai_cli import command_registry, help_panels, options
66
from aai_cli.app.context import run_with_options
77
from aai_cli.commands.dictate import _exec as dictate_exec
8+
from aai_cli.core import choices
89
from aai_cli.core.sync_stt import MAX_AUDIO_SECONDS
910
from aai_cli.ui.help_text import examples_epilog
1011

@@ -29,6 +30,7 @@
2930
"assembly dictate --word-boost AssemblyAI --word-boost LeMUR",
3031
),
3132
("One JSON object per utterance", "assembly dictate --json"),
33+
("Pipe the bare transcript onward", "assembly dictate -o text | assembly llm -f"),
3234
]
3335
),
3436
)
@@ -58,6 +60,12 @@ def dictate(
5860
max=float(MAX_AUDIO_SECONDS),
5961
),
6062
json_out: bool = options.json_option("Emit one JSON object per utterance"),
63+
output_field: choices.TextOrJson | None = typer.Option(
64+
None,
65+
"-o",
66+
"--output",
67+
help="Output mode: text (the bare transcript per utterance, pipe-friendly) or json",
68+
),
6169
) -> None:
6270
"""Push-to-talk dictation: record the mic, get the transcript back
6371
@@ -73,5 +81,6 @@ def dictate(
7381
device=device,
7482
once=once,
7583
max_seconds=max_seconds,
84+
output_field=output_field,
7685
)
7786
run_with_options(ctx, dictate_exec.run_dictate, opts, json=json_out)

aai_cli/commands/dictate/_exec.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,11 @@
1313
from dataclasses import dataclass
1414

1515
from aai_cli.app.context import AppState
16-
from aai_cli.core import sync_stt
16+
from aai_cli.core import choices, sync_stt
1717
from aai_cli.core.config_builder import split_csv
1818
from aai_cli.core.hotkey import CTRL_C, CTRL_D, ESC, TerminalKeys
1919
from aai_cli.core.microphone import MicrophoneSource
20+
from aai_cli.streaming.session import resolve_output_modes
2021
from aai_cli.ui import output
2122

2223
# Capture is resampled to one rate the Sync API accepts; 16 kHz mono PCM16 keeps
@@ -41,6 +42,8 @@ class DictateOptions:
4142
device: int | None
4243
once: bool
4344
max_seconds: float
45+
# -o/--output: text (the default bare-transcript shape) or json (== --json).
46+
output_field: choices.TextOrJson | None = None
4447

4548

4649
def _note(message: str, *, json_mode: bool, quiet: bool) -> None:
@@ -165,6 +168,11 @@ def _session(
165168

166169
def run_dictate(opts: DictateOptions, state: AppState, *, json_mode: bool) -> None:
167170
"""Execute one `assembly dictate` invocation from already-parsed flags."""
171+
# Fold -o/--output into json_mode (-o json == --json) and reject the
172+
# contradictory --json + -o text pair, the same way `stream`/`agent` do.
173+
# dictate has no live panel, so the text_mode half is unused — plain
174+
# transcript text is already the non-JSON default in `_emit`.
175+
_, json_mode = resolve_output_modes(opts.output_field, json_mode=json_mode)
168176
try:
169177
# Entering TerminalKeys validates the terminal (a usage precondition)
170178
# before credentials, so a piped stdin reads as "needs a terminal" — not

tests/__snapshots__/test_snapshots_help_run.ambr

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -373,6 +373,10 @@
373373
│ [default: 120.0] │
374374
│ --json -j Emit one JSON object per │
375375
│ utterance │
376+
│ --output -o [text|json] Output mode: text (the │
377+
│ bare transcript per │
378+
│ utterance, pipe-friendly) │
379+
│ or json │
376380
│ --help Show this message and │
377381
│ exit. │
378382
╰──────────────────────────────────────────────────────────────────────────────╯
@@ -388,6 +392,8 @@
388392
$ assembly dictate --word-boost AssemblyAI --word-boost LeMUR
389393
One JSON object per utterance
390394
$ assembly dictate --json
395+
Pipe the bare transcript onward
396+
$ assembly dictate -o text | assembly llm -f
391397

392398

393399

tests/test_dictate_exec.py

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@
1616

1717
from aai_cli.app.context import AppState
1818
from aai_cli.commands.dictate import _exec as dictate_exec
19-
from aai_cli.core import config, sync_stt
20-
from aai_cli.core.errors import CLIError
19+
from aai_cli.core import choices, config, sync_stt
20+
from aai_cli.core.errors import CLIError, UsageError
2121

2222
DICTATE_DEFAULTS = dictate_exec.DictateOptions(
2323
language=None,
@@ -145,6 +145,34 @@ def test_json_mode_emits_one_ndjson_object_per_utterance(seams, capsys):
145145
assert captured.err == ""
146146

147147

148+
def test_output_json_folds_into_ndjson_without_the_json_flag(seams, capsys):
149+
# -o json must enable NDJSON on its own (json_mode stays the --json flag,
150+
# which is False here) — proving the -o/--output resolution runs.
151+
seams["keys"] = FakeKeys(["\r", "\r"])
152+
_run(dataclasses.replace(DICTATE_DEFAULTS, output_field=choices.TextOrJson.json))
153+
assert json.loads(capsys.readouterr().out)["text"] == "hello world"
154+
155+
156+
def test_output_text_emits_bare_transcript(seams, capsys):
157+
# -o text is the explicit spelling of the human default: bare text, no JSON.
158+
seams["keys"] = FakeKeys(["\r", "\r"])
159+
_run(dataclasses.replace(DICTATE_DEFAULTS, output_field=choices.TextOrJson.text))
160+
out = capsys.readouterr().out
161+
assert out.strip() == "hello world"
162+
assert "{" not in out
163+
164+
165+
def test_output_text_conflicts_with_json_flag(seams):
166+
# --json + -o text are contradictory output shapes: a clean usage error,
167+
# the same as `stream`/`agent`.
168+
seams["keys"] = FakeKeys(["\r", "\r"])
169+
with pytest.raises(UsageError):
170+
_run(
171+
dataclasses.replace(DICTATE_DEFAULTS, output_field=choices.TextOrJson.text),
172+
json_mode=True,
173+
)
174+
175+
148176
def test_quiet_suppresses_the_interactive_hints(seams, capsys):
149177
seams["keys"] = FakeKeys(["\r", "\r"])
150178
_run(state=AppState(quiet=True))

0 commit comments

Comments
 (0)