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
63 changes: 55 additions & 8 deletions .github/scripts/verify_package.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,56 @@ def check_doctor(report: dict[str, object], service_status: str) -> None:
assert checks["service"]["status"] == service_status


def verify_controls(cli: Path, system: str, output_dir: Path) -> None:
if system not in {"Linux", "Windows"}:
return
if system == "Linux":
os.environ["XDG_DATA_HOME"] = str(output_dir / "xdg-data")
os.environ["XDG_CONFIG_HOME"] = str(output_dir / "xdg-config")

installed = run_cli(cli, "controls", "install", "--json")
assert installed["installed"] is True
assert installed["scheme"] == "agent-voice"
if system == "Linux":
desktop = Path(str(installed["path"]))
contents = desktop.read_text(encoding="utf-8")
assert "X-Agent-Voice-Owned=true" in contents
assert "-m agent_voice control-url %u" in contents
default = subprocess.run(
["xdg-mime", "query", "default", "x-scheme-handler/agent-voice"],
check=True,
capture_output=True,
text=True,
)
assert default.stdout.strip() == desktop.name
else:
import winreg

base = r"Software\Classes\agent-voice"
with winreg.OpenKey(winreg.HKEY_CURRENT_USER, base) as key:
assert winreg.QueryValueEx(key, "URL Protocol")[0] == ""
assert winreg.QueryValueEx(key, "AgentVoiceOwned")[0] == "1"
with winreg.OpenKey(
winreg.HKEY_CURRENT_USER, rf"{base}\shell\open\command"
) as key:
command = winreg.QueryValueEx(key, None)[0]
assert "-m agent_voice control-url" in command
assert '"%1"' in command

removed = run_cli(cli, "controls", "uninstall", "--json")
assert removed["removed"] is True
if system == "Linux":
assert not Path(str(installed["path"])).exists()
else:
try:
with winreg.OpenKey(winreg.HKEY_CURRENT_USER, base):
pass
except FileNotFoundError:
pass
else:
raise AssertionError("Windows control handler was not removed")


def main() -> None:
cli = Path(sys.argv[1]).resolve()
system = platform.system()
Expand All @@ -88,21 +138,19 @@ def main() -> None:
cli,
"speak",
f"{system} generation verification.",
"--service",
"off",
"--no-service",
"--output",
str(local_wav),
)
assert local["backend"] == "local"
assert local["played"] is False
assert "playback" not in local
validate_wav(local_wav)

labeled = run_cli(
cli,
"speak",
f"{system} labeled speed verification.",
"--service",
"off",
"--no-service",
"--label",
"Package E2E",
"--format",
Expand All @@ -122,6 +170,7 @@ def main() -> None:
== (Path(os.environ["AGENT_VOICE_HOME"]) / "recordings").resolve()
)
validate_mp3(labeled_path)
verify_controls(cli, system, output_dir)

log_path = output_dir / "service.log"
with log_path.open("w", encoding="utf-8") as log:
Expand Down Expand Up @@ -156,15 +205,13 @@ def main() -> None:
cli,
"speak",
f"{system} localhost service verification.",
"--service",
"on",
"--service-url",
SERVICE_URL,
"--output",
str(service_wav),
)
assert remote["backend"] == "service"
assert remote["played"] is False
assert "playback" not in remote
validate_wav(service_wav)
finally:
service.terminate()
Expand Down
9 changes: 3 additions & 6 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,7 @@ config.json
service-start.lock
viewer.lock
viewer.json
models/*.onnx
models/*.bin
models/*.lock
recordings/*
models/
recordings/
IDEAS.md
!models/.gitkeep
!recordings/.gitkeep
launch-video/
141 changes: 99 additions & 42 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,63 +33,94 @@ Brazilian Portuguese, though quality varies.

## Install and setup

Install the CLI and download the speech model:
Install the CLI with `uv`:

```sh
uv tool install agent-voice
agent-voice setup
```

### Choose a delivery method
Or with `pipx`:

```sh
pipx install agent-voice
```

For desktop applications, the recommended way is to use the `-desktop` skills.
They embed a media player directly in the answer and currently support
Antigravity, Codex, and OpenCode:
Then download the speech model:

```sh
npx skills add yoav0gal/agent-voice --skill create-speech-recording-desktop --global
npx skills add yoav0gal/agent-voice --skill spoken-response-desktop --global
agent-voice setup
```

For CLIs and portable Markdown delivery, install the normal skills:
For setup with experimental playback controls, see
[Experimental desktop playback controls](#experimental-desktop-playback-controls).

Test the CLI directly:

```sh
npx skills add yoav0gal/agent-voice --skill create-speech-recording --global
npx skills add yoav0gal/agent-voice --skill spoken-response --global
agent-voice speak "Hello from Agent Voice." -p
```

The normal skills deliver three links:
### Choose a skill

> [!Note]
> **The skills are starting points. Copy them to customize delivery wording,
> recording defaults, playback behavior, or when an agent should offer audio to
> your liking!**

Agent Voice provides two kinds of skill:

- **`create-speech-recording`** turns supplied text into audio. Use it to create
a recording or read something aloud.
- **`spoken-response`** creates the spoken semantic twin of an assistant
response. It can speak the current response, the previous response, or later
responses in the thread.

Choose how the recording should appear in your agent:

| Variant | Delivery format | Supported surfaces |
| --- | --- | --- |
| Normal | Portable Markdown links to the web player, media app, and web audio | Anywhere basic links can be clicked |
| `-desktop` | Embedded native or HTML audio player | Codex Desktop, Antigravity, and OpenCode Desktop |
| ⚠️ `-controls` ⚠️ | Clickable `agent-voice://` playback links with a web-player fallback | Compatible desktop renderers on macOS, Linux, and Windows |

For portable delivery, install the normal skills:

```sh
npx skills add yoav0gal/agent-voice -g --skill create-speech-recording
npx skills add yoav0gal/agent-voice -g --skill spoken-response
```

- **web player** — the recommended option; open it in an application browser.
In coding-agent apps, this view includes the written answer too.
- **media app** — opens the linked recording in your default media app.
- **web audio** — opens the audio directly and should start playing
automatically.
<p align="center">
<img src="assets/screenshots/portable-delivery.png" alt="Portable Agent Voice delivery with listening links and a terminal playback command" width="900">
</p>

Test it:
For an embedded player in a supported desktop app, install the desktop skills:

```sh
agent-voice speak "Hello from Agent Voice." --play
npx skills add yoav0gal/agent-voice -g --skill create-speech-recording-desktop
npx skills add yoav0gal/agent-voice -g --skill spoken-response-desktop
```

## Skills
<p align="center">
<img src="assets/screenshots/desktop-delivery.png" alt="Agent Voice audio embedded natively inside a desktop conversation" width="720">
</p>

Agent Voice includes two skills:
### Experimental desktop playback controls

- **create-speech-recording** turns supplied text into audio. Use it when you
want an agent to create a recording or read something aloud.
- **spoken-response** creates an audio version of an agent's written response.
Use it when you want to listen to a long answer instead of reading it.
> [!WARNING]
> Experimental feature.

Each skill also has a **`-desktop`** version that does the same job but
delivers the recording through a player embedded in the desktop app.
```sh
agent-voice controls install
npx skills add yoav0gal/agent-voice -g --skill create-speech-recording-controls
npx skills add yoav0gal/agent-voice -g --skill spoken-response-controls
```

These skills are starting points. Copy them, edit them, and make them yours.
You can change delivery wording, recording defaults, playback behavior, or
when the agent should offer audio.
<p align="center">
<img src="assets/screenshots/controls-delivery.png" alt="Experimental Agent Voice playback controls with a web-player fallback" width="800">
</p>

Each skill owns its delivery references. The CLI returns structured facts; the
installed skill decides how those facts are presented.
Remove the handler with `agent-voice controls uninstall`.

## CLI

Expand All @@ -99,13 +130,16 @@ The CLI provides small primitives that agents can combine. Run
| Command | What it does |
| --- | --- |
| `setup` | Download and verify speech model assets. |
| `update` | Upgrade Agent Voice through its `uv` or `pipx` installer. |
| `speak` | Turn text or stdin into a recording. |
| `play` | Play an existing local recording. |
| `voices` | List supported language tags and voices. |
| `models` | List speech models and variants. |
| `config` | View or change persistent defaults. |
| `doctor` | Check that Agent Voice is ready. |
| `service start\|stop` | Manage the background speech service. |
| `viewer start\|stop` | Manage the local recording viewer. |
| `controls install\|uninstall` | Install or remove the experimental desktop protocol handler. |
| `serve` | Start the localhost speech API. |

### Speak
Expand All @@ -127,7 +161,7 @@ agent-voice speak --response-file "$RESPONSE_AS_MARKDOWN_FILE" \

# Choose the output and delivery
agent-voice speak "Here is your summary." \
--voice bf_emma --speed 1.2 --format mp3 --play
--voice bf_emma --speed 1.2 --format mp3 -p
```

| Option | Purpose |
Expand All @@ -141,13 +175,14 @@ agent-voice speak "Here is your summary." \
| `-v, --voice NAME` | Select a voice. |
| `--lang TAG` | Set the language tag (default: `en-us`). |
| `--speed NUMBER` | Set pitch-preserving playback speed. |
| `--play` | Play the recording after creation. |
| `--service on\|off\|timed` | Control background inference. |
| `--service-timeout MINUTES` | Set the idle timeout for timed mode. |
| `-p, --play` | Start local playback after creation, without waiting for it to finish. |
| `--play-after SECONDS` | Schedule local playback after creation, without waiting. |
| `--controls` | Include experimental desktop playback control links. |
| `--no-service` | Run the same Agent Voice model inside this command, then unload it. |
| `--model-id ID`, `--variant NAME` | Select a model and build. |

`speak` prints one JSON receipt with the absolute recording path, file URI,
audio metadata, playback status, and available viewer links. This makes the
audio metadata, playback state (`started` or `scheduled`), and available viewer links. This makes the
command reliable for both people and agents.

### Configure defaults
Expand All @@ -156,16 +191,33 @@ command reliable for both people and agents.
# Show current defaults
agent-voice config

# Set your preferred voice, speed, format, and output directory
agent-voice config --voice bf_emma --speed 1.15 --format mp3 --output-dir ./recordings
# Set your preferred voice, speed, format, service timeout, and output directory
agent-voice config --voice bf_emma --speed 1.15 --format mp3 \
--service-timeout 10 --output-dir ./recordings

# Restore built-in defaults
agent-voice config --reset
```

The same values can be overridden per recording with `speak`. Service modes are
`on` for a persistent local service, `off` for embedded inference, and `timed`
to stop the service after an idle timeout.
Voice, speed, format, and output directory can be overridden per recording with
`speak`.

### Manage the Agent Voice service

By default, `speak` starts the Agent Voice background service when needed. Once
the model weights are loaded, the service keeps them warm between requests to
avoid another cold startup. It stops after 10 idle minutes by default, and each
completed speech request restarts that timer. Automatic startup reuses a running
service without changing its timeout; `service start` uses the saved timeout or
an explicit `--idle-timeout` value.

```sh
agent-voice service start # stops after 10 idle minutes
agent-voice service start --idle-timeout 30 # set this process to 30 minutes
agent-voice service stop
```

Use `agent-voice serve` for a foreground service while debugging.

### Discover and diagnose

Expand All @@ -186,6 +238,7 @@ agent will consume the result.

```sh
agent-voice play "/absolute/path/recording.mp3"
agent-voice play "/absolute/path/recording.mp3" --after 10
agent-voice viewer start
agent-voice viewer stop
```
Expand All @@ -199,6 +252,10 @@ language metadata are left alone. Opening a player or audio URL regenerates
missing audio from its source with the original language and current voice and
speed.

Playback commands return as soon as local playback starts, or immediately with
`scheduled` when a delay is requested; they never wait for the recording to end.

> [!Note]
> 🗒️ The viewer is a workaround for agent surfaces that do not support embedded
audio. I expect native text-to-speech to become common across these platforms,
which would be a better solution. For now, the viewer keeps playback and the
Expand Down
Binary file modified assets/agent-voice-launch.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/screenshots/controls-delivery.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/screenshots/desktop-delivery.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/screenshots/portable-delivery.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading