Running VoiceStudio as a Server on Your LAN
VoiceStudio is an open source desktop app that designs a voice from a text description and generates speech on the PC's GPU. Its backend listens on port 3900 of the PC's loopback address, so no other device can reach it. I wanted a tablet on my home network to request speech from the voices I designed, which meant the backend had to listen on the PC's LAN address with the same settings every time the app starts. This post covers the four changes on the PC, how to check them, and what went wrong along the way. The app is on GitHub.
Bind the backend to all interfaces
The backend is FastAPI served by uvicorn. VoiceStudio documents an OMNIVOICE_BIND_HOST=0.0.0.0 variable, but it has no effect in the desktop app. The Electron launcher starts uvicorn with --host 127.0.0.1 on the command line, and a command line flag overrides the variable.
The launcher does honor OMNIVOICE_BACKEND_CMD, which replaces the whole backend command. I found it by reading the launcher code in resources/app.asar, and the documentation does not mention it. Set it as a user environment variable in PowerShell.
$py = "$env:APPDATA\VoiceStudio\runtime\project\.venv\Scripts\python.exe"
$appdir = "$env:APPDATA\VoiceStudio\runtime\project\backend"
[Environment]::SetEnvironmentVariable('OMNIVOICE_BACKEND_CMD', "$py -m uvicorn main:app --app-dir $appdir --host 0.0.0.0 --port 3900", 'User')
Write the value as plain text separated by spaces. A JSON array also works in principle, but setx removes the quotes, the launcher fails to parse the value, and it falls back to the stock command without a message. Plain text works as long as no path contains a space.
Put the settings in the env file
The backend's main.py reads %USERPROFILE%\.config\omnivoice\env when it starts, and the file overrides anything the launcher injected. The file is named env with no extension and holds one KEY=value pair per line. Three settings go in it.
OMNIVOICE_API_KEY=your-long-random-key OMNIVOICE_TRUSTED_NETWORKS=192.168.1.0/24 OMNIVOICE_ALLOWED_ORIGINS=null,http://localhost:3901,http://127.0.0.1:3901,tauri://localhost,http://tauri.localhost,app://voicestudio
OMNIVOICE_API_KEY protects the admin routes (settings, env, and network share) from every machine except the PC itself. Generate it with a cryptographically secure random generator. VoiceStudio's own documentation suggests 32 random bytes encoded as base64url.
OMNIVOICE_TRUSTED_NETWORKS lets a client on a listed subnet list voices and generate speech with no credential, while admin routes still need the key. This is why the tablet carries no secret. Any device on a listed subnet can generate speech without a credential, so list only your own LAN subnet and replace the example above with it.
OMNIVOICE_ALLOWED_ORIGINS is the CORS list. A page opened from a file:// URL sends the literal origin null, so null has to be in the list for an offline HTML app to call the server.
Check the file's keys before you rely on it. My first attempt lost a newline and became one merged line, which set neither key. Later the key line had the origins line attached to it, and the backend enforced a 139 character "key". This command prints the keys as the parser sees them.
Get-Content $env:USERPROFILE\.config\omnivoice\env | % { ($_ -split '=',2)[0] }
Open the firewall
Allow inbound TCP on port 3900 for the Private profile and confirm the Wi-Fi or Ethernet network is set to Private.
New-NetFirewallRule -DisplayName "VoiceStudio LAN 3900" -Direction Inbound -Protocol TCP -LocalPort 3900 -Action Allow -Profile Private
Restart VoiceStudio completely
Closing the window leaves VoiceStudio in the tray with the backend still running, and the backend reads the env file only at start. Stop every process, then launch the app from the Start menu so it picks up the new user variable. A terminal opened before setx still carries the old environment.
Get-Process VoiceStudio, python, voicestudio-desktop-bridge -ErrorAction SilentlyContinue | Stop-Process -Force
The backend comes up on the LAN about 40 seconds after the app starts, and nothing needs to be configured again at the next start.
Check that the server is healthy
Run these on the PC and replace 192.168.1.20 with the PC's LAN address.
# bound to all interfaces? expect 0.0.0.0
Get-NetTCPConnection -LocalPort 3900 -State Listen | Select LocalAddress
# which command is the backend running?
(Get-CimInstance Win32_Process -Filter "name='python.exe'" | ? CommandLine -match uvicorn).CommandLine
# voices reachable on the LAN address with no credential (trusted subnet)
(Invoke-RestMethod http://192.168.1.20:3900/v1/audio/voices).voices | ? type -eq profile | % name
# admin still gated, expect Unauthorized
try { Invoke-RestMethod http://192.168.1.20:3900/system/network/enable -Method Post } catch { $_.Exception.Response.StatusCode }
The voices call should list every profile you saved. From another device on the subnet, request /health and /v1/audio/voices on the same address and port.
If the backend is listening on 127.0.0.1 again after a VoiceStudio update, the launcher no longer honors the override or the runtime path has moved. Check the command line from the second command and the OMNIVOICE_BACKEND_CMD user variable.
The built-in network share
The app also has a Share on your network feature that serves the same API on port 3901 with a PIN. I did not use it because it is not kept across restarts and the PIN changes each time the feature is turned on, so every restart would mean updating each client.
Using it from a managed device
I build ET Ducky, and the tablet that calls this server is managed from the ET Ducky dashboard. The ARM64 tablet post covers how the tablet's voice settings are changed from there.
Try it on one server
One Windows or Linux server on the Free query tier with your own AI key costs $5 a month.
See pricing