VoiceTalk v3 — Linux CLI + Web Remote
Control your Linux PC from your phone via a simple web interface. See what's playing, what's open, what apps are running — and control it all with a dropdown of concrete actions. No app installs, no voice, no AI guessing. Just you, your PC state, and pre-configured commands.
Features
- Real-time state display — See MPRIS players, open windows, running apps, and system audio.
- Launch installed apps — Search everything with a
.desktopentry and start it from the phone, whether or not it is already running. - YouTube search & play — Search YouTube videos, see results with title, channel and duration, and tap to play in your browser. The video starts on its own and shows up under Players — vt checks the browser's autoplay policy and can fix it from the phone if it would block playback.
- YouTube playback control — When a YouTube video plays in your browser, control it from the phone: play/pause, seek, volume, fullscreen, close (requires
xdotoolandwmctrl). - Capability-aware controls — The phone shows only the actions each player/window/app actually supports (play/pause, next/prev, seek, focus, close, mute, volume).
- Pre-configured commands — Define shell commands in TOML once, invoke them from the phone by name. No arbitrary text input.
- Cloudflare Tunnel —
make devstarts a quick tunnel by default, giving you a*.trycloudflare.comURL accessible from anywhere. No port forwarding, no router config, no Cloudflare account needed. - Device pairing — off-network callers must present a paired-device credential; the startup token is only accepted on the LAN. Pair a phone with
vt pairor the QR code printed at startup. - Web UI, no app install — Open
http://<pc-ip>:8765in any phone browser. No APK, no build step, bookmarkable. - Token auth — Printed on startup, hidden from history. Works on trusted LANs.
- Linux-native — MPRIS over D-Bus, PipeWire volume, psutil app detection, systemd services. Built for GNOME/Wayland.
Installation
Prerequisites
- Linux PC (Ubuntu 22.04+, Fedora 37+, or similar with systemd + PipeWire/ALSA + GNOME Shell 45+)
- Python 3.11+ (check:
python3 --version) - Phone with a modern browser (same WiFi network or routed access)
Quick Install (PyPI)
One command to install everything:
curl -fsSL https://raw.githubusercontent.com/Vaithorat/voicetalk/main/install.sh | bash
This script:
- Detects your Linux distro (Debian/Ubuntu or Fedora)
- Installs system dependencies (
python3-dbus,python3-gi) - Installs VoiceTalk from PyPI
- Prints next steps
After installation:
-
Verify preflight checks
vt doctorAll lines should be ✓ (or ℹ for optional features). If D-Bus or wpctl fail, your system cannot run VoiceTalk.
-
(Optional) Install the window control extension
vt install-extensionThis enables the
FocusandClosebuttons for open windows. On Wayland, you must log out and log back in for the extension to activate. -
(Optional) Configure custom commands
mkdir -p ~/.config/voicetalk cp ~/.local/lib/python*/site-packages/vt/commands.toml.example ~/.config/voicetalk/commands.toml nano ~/.config/voicetalk/commands.toml
See the example file for syntax. Commands are validated on startup and invalid entries are skipped with a warning.
Development Setup (from source)
For development or to work on the code:
-
Clone and enter the repo
git clone https://github.com/Vaithorat/voicetalk cd voicetalk
-
Install system dependencies (same as above)
sudo apt-get install python3-dbus python3-gi
-
Set up development environment
make setupCreates a venv and installs the package in editable mode with dev/test extras.
-
Run tests and linting
make test make lint
-
Start the development server
make dev
Quick Start
-
Start the server
make devmake devis the only start command you need. It sets the environment up if it isn't already, then runs the server throughvenv/bin/vtby absolute path — so it behaves identically from a VS Code terminal, a plain shell, or any other directory. A Cloudflare tunnel is started by default.Output:
VoiceTalk → http://192.168.1.5:8765/?t=Xq3v... Token: Xq3v... ⏳ Starting Cloudflare Tunnel... ── Cloudflare Tunnel is up ──────────────────── Public URL: https://some-words.trycloudflare.com ── Pair a device ────────────────────────────── Code: RRMFH-2QK9X (valid 10 min, one device) Link: https://some-words.trycloudflare.com/?p=RRMFH2QK9X -
Open the URL on your phone (or scan the QR code)
- From the LAN: the token is stored in
localStorage, bookmark works. - From anywhere: enter the pairing code shown at startup.
- From the LAN: the token is stored in
-
Control your PC
- Select a target (media player, window, app, command)
- Click
Actionsto expand the dropdown - Adjust sliders or tap buttons
Commands
| Command | Purpose |
|---|---|
vt serve [--host IP] [--port 8765] [--no-token] [--open] [--tunnel] |
Start the HTTP server. Default: LAN IP with Cloudflare tunnel. --tunnel enables a quick tunnel for global access. --no-token disables token auth. --open opens in browser. |
vt pair [--url URL] [--port PORT] [--minutes N] |
Issue a one-time pairing code for a new device. Prints a link and QR code. |
vt devices [--revoke ID] [--revoke-all] |
List paired devices or revoke access. |
vt audit [-n N] [--rejects] |
Show recent security log entries. |
vt status |
Print the current state as a terminal table (no web server). |
vt do <target-id> <action-id> [value] |
Invoke an action from the CLI. For testing. |
vt commands |
List configured commands. |
vt apps [query] |
List installed apps you can launch, optionally filtered (vt apps browser). Launch one with vt do launcher:<id> launch. |
| YouTube Search | Find and play YouTube videos from the phone UI (with yt-dlp installed) |
vt allow-autoplay [--status] [--revert] [--restart] |
Let the browser start videos opened from the phone. Writes media.autoplay.default into the Firefox profile's user.js — the same setting as Settings → Privacy & Security → Autoplay → Allow Audio and Video. Takes effect on the next Firefox start; --restart does that for you. |
vt doctor |
Run preflight checks. |
vt install-extension |
Install the GNOME Shell window control extension. |
HTTP API
The web UI speaks several endpoints. Token auth via X-VT-Token header for
LAN; device auth via X-VT-Device + X-VT-Secret headers for remote access.
GET /api/session
→ {authenticated, kind, device_id, remote, needs_pairing}
POST /api/pair
← {code, name}
→ {ok, device_id, secret, name}
POST /api/pair/self
← {name}
→ {ok, device_id, secret, name}
GET /api/devices
→ {devices: [...], current: device_id}
POST /api/devices/revoke
← {id}
GET /api/state
→ {"targets": [...], "ts": unix_timestamp}
GET /api/apps[?q=search+terms]
→ {"apps": [{"id": "launcher:firefox", "title": "Firefox", ...}]}
GET /api/youtube[?q=search+terms]
→ {"results": [...], "error": ""}
POST /api/do
← {"target": "kind:id", "action": "action-id", "value": float?}
→ {"ok": bool, "message": "..."}
Installed apps are deliberately not part of /api/state: there are hundreds of
them and they change about once a week, so they would dwarf the state that
actually moves in a 1 Hz poll. The phone fetches /api/apps once, when you open
the list, and filters as you type.
Configuration
commands.toml
Place at ~/.config/voicetalk/commands.toml to add custom commands.
[[command]]
id = "lock"
label = "🔒 Lock Screen"
run = ["loginctl", "lock-session"]
[[command]]
id = "suspend"
label = "💤 Suspend"
run = ["systemctl", "suspend"]
confirm = true # Require double-tap
Rules:
runis always a list of arguments (shell=False), never a string. This is the security boundary.idmust be unique and cannot collide with built-in actions.- If
confirm: true, the phone requires a second tap ("Sure?") before executing. - Invalid entries are logged and skipped on startup.
Internals
- Targets — Everything controllable (media players, windows, apps, system controls, commands) is a Target with a list of Actions.
- Sources — Targets come from:
- MPRIS (
sources/mpris.py) — Media players (Firefox, Chrome, VLC, Spotify, etc.) - Windows (
sources/windows.py) — Open windows via GNOME Shell extension (optional) - Apps (
sources/apps.py) — Running apps matched against.desktopfiles, and every installed.desktopentry as a launchable target - Audio (
sources/audio.py) — System volume viawpctl - YouTube (
sources/youtube.py) — Search viayt-dlp, and opening a video in the browser. Before it opens one it askssources/browser_autoplay.pywhether the browser will actually start playing, so a tap that cannot succeed says why instead of reporting success. - Commands (
commands.py) — User-defined shell commands from TOML
- MPRIS (
- Actions — Derived from player capabilities (CanPlay, CanPause, CanSeek, etc.) so unsupported actions don't appear.
- State refresh — 1 Hz background task. The web UI polls instantly; the server caches.
Limitations
- Wayland — Keystroke injection is not possible; the old F11 fullscreen / arrow seek are gone.
- MPRIS only — Only players that register on D-Bus appear (Firefox, Chrome, VLC, mpv, Spotify). HTML5
<video>without a media session will not. - Window extension — Requires GNOME Shell 45+, needs a logout/login to activate, and may need a
metadata.jsonupdate for GNOME 51+. On X11 or KDE, the feature doesn't work. - Autoplay is a browser setting — vt can read and set it for Firefox (
vt allow-autoplay), but it only takes effect on the next Firefox start, and for Chromium-family browsers there is no equivalent switch from outside the process. - Plain HTTP — The token stops casual access on a trusted network; it is not TLS. The Cloudflare tunnel provides HTTPS end-to-end.
Troubleshooting
No targets appear on the phone:
- Run
vt doctorand fix any failures. - Check the server logs:
vt serveprints errors to stdout. - Ensure your phone and PC are on the same network (or route exists).
Window actions not working:
- Run
vt install-extensionand log out/in. - Check:
gnome-extensions list | grep voicetalkshould showvoicetalk@local. - Check D-Bus:
gdbus call --session --dest org.gnome.Shell.Extensions.VoiceTalk --object-path /org/gnome/Shell/Extensions/VoiceTalk --method org.gnome.Shell.Extensions.VoiceTalk.List
Commands not executing:
- Check
~/.config/voicetalk/commands.tomlexists and parses:python3 -c "from vt.commands import CommandsConfig; c = CommandsConfig(); print(c.get_errors())". - Ensure
runis a list, not a string:run = ["systemctl", "suspend"]notrun = "systemctl suspend".
Media players missing, or the log repeats "Introspect error ... AccessDenied":
- You started
vtfrom inside a snap's built-in terminal (the VS Code snap's, most often). Its children inherit the snap's AppArmor label, and snap policy then blocks them from talking to other snaps — so snap-packaged Firefox refuses every property read and its player vanishes from the phone. - Fix: run
vt servefrom an ordinary terminal (GNOME Terminal).vt doctorreports the confinement underConfinementandMPRIS.
A video opens on the PC but sits there paused:
- The browser is blocking autoplay. Firefox blocks audible autoplay by default, so the tab loads, nothing plays, no MPRIS player is published, and the video never reaches the Players list — from the phone it looks like the tap did nothing at all.
- Fix from the phone: open YouTube Search; the banner at the top offers Allow autoplay, which sets the pref and restarts Firefox (tabs are restored) so the video you just picked starts playing.
- Fix from the PC:
vt allow-autoplay, then restart Firefox — or set Settings → Privacy & Security → Autoplay to Allow Audio and Video yourself. vt doctorreports this underAutoplay, andvt allow-autoplay --statusshows which profile it read.- Undo with
vt allow-autoplay --revert. Note that Firefox copies the setting into its ownprefs.jsonce it has started with it, so a revert removes vt's override but the value can persist — the command says so when that happens, and the Settings UI is then the way back.
YouTube playback controls don't appear:
- Make sure
xdotoolandwmctrlare installed:apt install xdotool wmctrl - Open a YouTube video in Firefox or Chrome
- Check that the video window title contains "youtube" or "youtube.com"
Volume slider unresponsive:
- Check
wpctl statusoutput. If no sinks, PipeWire is not running or misconfigured.
Security
Two tiers of access, deliberately unequal:
- LAN — the startup token in the URL is enough. It travels in a bookmark and a QR code, which is fine for a network you already control.
- Remote — the token is not accepted at all. The caller must present a paired-device credential, and a device is paired once, from a code that only ever appears on this PC's own terminal.
That split is the whole security model: exposing the public URL leaks nothing, because the URL is not a credential off-network.
- Device pairing — 31^10 entropy codes, 10-minute TTL, single-use. Pair a
device with
vt pairor the QR printed at startup. Max 32 devices. - Rate limiting — 5 failed auth attempts per IP triggers a 15-minute lockout. Pairing attempts are rate-limited globally (30/hour).
- Audit log — every authenticated action and rejected attempt is recorded
in
~/.local/state/voicetalk/audit.log. View withvt audit. - Security headers — CSP, HSTS, X-Frame-Options, nosniff on every response.
- Command validation — Commands are defined in a TOML file on the PC; the phone can only name one, not supply arguments.
- No secrets stored — V3 has no API keys, no SMTP credentials, no encryption at rest. Device secrets are SHA-256 hashed.
Project Structure
voicetalk/
├── vt/
│ ├── __init__.py
│ ├── cli.py # CLI entry point
│ ├── model.py # Target/Action dataclasses
│ ├── state.py # Snapshot assembly
│ ├── server.py # aiohttp HTTP server
│ ├── auth.py # Device pairing, credentials, rate limiting
│ ├── tunnel.py # Cloudflare Tunnel integration
│ ├── commands.py # TOML commands loader
│ ├── sources/
│ │ ├── mpris.py # MPRIS players
│ │ ├── windows.py # GNOME extension
│ │ ├── apps.py # Running and installed apps
│ │ ├── audio.py # PipeWire volume
│ │ ├── youtube.py # Search, and opening a video so it plays
│ │ ├── youtube_player.py # X11-only keystroke fallback
│ │ └── browser_autoplay.py # Whether the browser will actually start it
│ ├── ui/
│ │ └── index.html # Single-file web UI
├── gnome-extension/voicetalk@local/
│ ├── metadata.json
│ ├── extension.js # D-Bus window interface
├── Makefile # make dev / setup / test / doctor
├── scripts/envreport.py # backs `make env`
├── pyproject.toml
├── commands.toml.example
├── README.md
└── tests/ # pytest suite (WIP)
Development
Everything goes through make. Each target sets the environment up first if it
needs to, so there is no activate step and no ordering to remember.
| Target | What it does |
|---|---|
make dev |
Set up if needed, then start the server. Start here. |
make setup |
Create venv/ and install deps. Idempotent; re-runs only when pyproject.toml changes. |
make test |
Run the pytest suite (make test ARGS="-x -k mpris"). |
make doctor |
Preflight checks — D-Bus, PipeWire, port, extension, config. |
make status / make commands / make apps |
CLI passthroughs. |
make env |
Print the resolved interpreter and which optional deps it can see. |
make link / make unlink |
Add/remove ~/.local/bin/vt. |
make deps |
Force a dependency reinstall. |
make clean |
Drop caches and build artifacts (keeps the venv). |
make reset |
Delete the venv and rebuild from scratch (~10s). |
Options apply to make dev: HOST=0.0.0.0, PORT=9000, NO_TOKEN=1, OPEN=1,
and ARGS="..." for anything else.
Why make, and not python3 -m vt serve
python3 -m vt serve resolves to a different interpreter depending on where you
run it. A VS Code terminal auto-activates venv/ and gets yt-dlp; a plain
shell gets the system Python and doesn't, so YouTube search quietly stops
working — and from any directory other than the repo root the import fails
outright. The Makefile removes the ambiguity:
- every path is derived from the Makefile's own location, never from
$PWD - every command runs
venv/bin/pythonorvenv/bin/vtby absolute path VIRTUAL_ENV,PYTHONPATH, andPYTHONHOMEfrom the calling shell are dropped, so an unrelated activated venv cannot change the result
If two terminals still disagree, run make env in both and compare the
interpreter line.
License
MIT (see LICENSE file, coming soon)
Acknowledgments
- Built on MPRIS (freedesktop.org), PipeWire (pipewire.org), GNOME Shell, and aiohttp.
- Inspired by the original VoiceTalk v1–v2, refactored for simplicity and Linux-native APIs.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file voicetalk-3.1.0.tar.gz.
File metadata
- Download URL: voicetalk-3.1.0.tar.gz
- Upload date:
- Size: 102.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
13fcaa68986557202a8579ef1c8ee0349116155334201457b612b35599b64ee7
|
|
| MD5 |
5d9ad4126692ffc3979e4dd73ab093ec
|
|
| BLAKE2b-256 |
98f5f50cee3b85074e9bb86726c9bd52a00357932a6cb6640d51578625291632
|
Provenance
The following attestation bundles were made for voicetalk-3.1.0.tar.gz:
Publisher:
publish.yml on Vaithorat/voicetalk
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
voicetalk-3.1.0.tar.gz -
Subject digest:
13fcaa68986557202a8579ef1c8ee0349116155334201457b612b35599b64ee7 - Sigstore transparency entry: 2579632381
- Sigstore integration time:
-
Permalink:
Vaithorat/voicetalk@a20cd8ddd69eaefacd20183d447a18800cfb7069 -
Branch / Tag:
refs/tags/v3.1.0 - Owner: https://github.com/Vaithorat
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@a20cd8ddd69eaefacd20183d447a18800cfb7069 -
Trigger Event:
release
-
Statement type:
File details
Details for the file voicetalk-3.1.0-py3-none-any.whl.
File metadata
- Download URL: voicetalk-3.1.0-py3-none-any.whl
- Upload date:
- Size: 75.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
348dc0df06d4eb383f3241be8bdaaafc4bcec55c7ea0682998f17e01f1b9ec77
|
|
| MD5 |
8ce053aeefdd40c2c3393431a3294d77
|
|
| BLAKE2b-256 |
b3f65a220fa23dbab8d7bac8295a47194d247c7249314127fd8823835484a737
|
Provenance
The following attestation bundles were made for voicetalk-3.1.0-py3-none-any.whl:
Publisher:
publish.yml on Vaithorat/voicetalk
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
voicetalk-3.1.0-py3-none-any.whl -
Subject digest:
348dc0df06d4eb383f3241be8bdaaafc4bcec55c7ea0682998f17e01f1b9ec77 - Sigstore transparency entry: 2579632403
- Sigstore integration time:
-
Permalink:
Vaithorat/voicetalk@a20cd8ddd69eaefacd20183d447a18800cfb7069 -
Branch / Tag:
refs/tags/v3.1.0 - Owner: https://github.com/Vaithorat
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@a20cd8ddd69eaefacd20183d447a18800cfb7069 -
Trigger Event:
release
-
Statement type: