Compare commits

...
38 changed files with 463 additions and 208 deletions
+1 -1
View File
@@ -45,6 +45,6 @@ body:
- type: markdown
attributes:
value: |
> OmniVoice is **local-first** — features must work fully offline with no accounts,
> VoiceStudio is **local-first** — core features work offline without an account,
API keys, or cloud calls, and behave identically on macOS/Windows/Linux. Proposals
that fit those constraints are easiest to land.
+1 -1
View File
@@ -29,7 +29,7 @@
# On main pushes the Docker Hub repository overview is also synced from
# deploy/dockerhub-overview.md (source of truth for the hub.docker.com page).
#
# NOTE: the Docker image is the headless web-server build of OmniVoice (FastAPI
# NOTE: the Docker image is the headless web-server build of VoiceStudio (FastAPI
# backend + pre-built React frontend served over HTTP). The Tauri desktop
# auto-updater and its update-channel toggle are desktop-only features; they do
# NOT apply to the Docker image.
+6
View File
@@ -8,8 +8,14 @@ The bundled TTS model package (`pyproject.toml`) is versioned independently.
## [Unreleased]
## [0.5.0] — 2026-08-10
**Highlights**
- VoiceStudio 0.5.0 is the release where the name, desktop chrome, documentation, and package metadata finally tell one clear story
- A local-first creative studio: voice cloning, design, dubbing, dictation, stories, audiobooks, and transcription without a subscription meter
- Reliability first: automatic cache repair, truthful hardware routing, safer sidecars, and actionable recovery instead of mystery failures
- Security boundaries now match the product: native file access stays native, untrusted network destinations fail closed, and public errors keep private diagnostics local
- RTX 40-series GPUs are used again instead of being sent to the CPU
- A warning before a slow generation, rather than after a five-minute wait
- The watermark can be turned off in Settings, as the docs always said
+13 -58
View File
@@ -2,13 +2,13 @@
<img src="docs/logo.png" alt="VoiceStudio Logo" width="120" />
<h1>VoiceStudio</h1>
<p><sub><em>previously OmniVoice-Studio</em></sub></p>
<h3>The open-source ElevenLabs alternative.</h3>
<p>Real-time dictation, zero-shot voice cloning, and cinematic video dubbing — all on your desktop.<br/><b>No accounts. No API keys. No cloud.</b> Everything runs on your machine. Open-source, <b>646 languages.</b></p>
<h3>Make voices. Tell stories. Keep the files. ♡</h3>
<p>Clone, design, dub, dictate, and build audiobooks in one open-source desktop studio.<br/><b>Local-first by default.</b> No subscription or usage meter. Optional online services stay opt-in.</p>
<p>
<a href="#quickstart">Quickstart</a> ·
<a href="#features">Features</a> ·
<a href="#why-ovs">vs Others</a> ·
<a href="#why-voicestudio">Why VoiceStudio</a> ·
<a href="#tts-engines">Engines</a> ·
<a href="#openai-api">API</a> ·
<a href="#sponsor--donate">Donate</a> ·
@@ -48,56 +48,11 @@
<img src="docs/screenshot-launchpad.png" alt="VoiceStudio — Launchpad" width="100%"/>
</div>
> **Your voice is the most personal data you have. So why rent it back from a cloud?** Every mainstream voice tool ships your audio to someone else's server and bills you monthly for the privilege. VoiceStudio flips that: clone, design, dub, and dictate on your own hardware — 646 languages, no meter running, nothing leaving your machine.
> **Your voice is personal. Your studio should feel personal too.** VoiceStudio keeps its core workflow on your hardware: clone, design, dub, dictate, and publish in 646 languages without a subscription or usage meter. Network-backed engines and services are optional, visible choices—not hidden requirements.
> [!WARNING]
> **Active beta.** Things may break between releases — for the newest fixes, run from source. Bug reports and PRs are very welcome: [open an issue](https://github.com/debpalash/VoiceStudio/issues) or [join Discord](https://discord.gg/bzQavDfVV9).
<a id="screenshots"></a>
## 📸 See it in action
<table>
<tr>
<td align="center" width="50%">
<img src="docs/screenshot-studio.png" alt="Studio" width="100%"/>
<br/><b>Studio</b><br/>
<sub>Generate &amp; clone in one workspace — a 3-second clip mirrors any voice, 646 languages, zero-shot.</sub>
</td>
<td align="center" width="50%">
<img src="docs/screenshot-design.png" alt="Voice Design" width="100%"/>
<br/><b>Voice Design</b><br/>
<sub>Build new voices from scratch — gender, age, accent, pitch, emotion, dialect.</sub>
</td>
</tr>
<tr>
<td align="center">
<img src="docs/screenshot-gallery.png" alt="Voice Gallery" width="100%"/>
<br/><b>Voice Gallery</b><br/>
<sub>Browse ready-made archetype voices with language filters, or build your own — then pick any of them in Studio, Audiobook, Stories, and Dubbing.</sub>
</td>
<td align="center">
<img src="docs/screenshot-dub.png" alt="Video Dubbing" width="100%"/>
<br/><b>Video Dubbing</b><br/>
<sub>A real dub, end to end: 37 segments transcribed, translated to Bengali, re-voiced, and timed — ready to export as MP4.</sub>
</td>
</tr>
<tr>
<td align="center">
<img src="docs/screenshot-engines.png" alt="Settings — Engines" width="100%"/>
<br/><b>Settings → Engines</b><br/>
<sub>The engine compatibility matrix — 14 TTS engines with per-engine GPU preflight, no silent CPU fallback.</sub>
</td>
<td align="center">
<img src="docs/screenshot-settings.png" alt="Settings — Models" width="100%"/>
<br/><b>Settings → Models</b><br/>
<sub>One-click model store — auto-detects your platform (CUDA / MPS / CPU) and recommends the right models.</sub>
</td>
</tr>
</table>
---
<a id="features"></a>
## ✨ Features
@@ -122,7 +77,7 @@ Three flagships, five more headliners, and a dozen under the fold.
<td align="center" width="20%">📖<br/><b>Audiobook</b><br/><sub>EPUB/PDF → .m4b, multi-voice cast</sub></td>
<td align="center" width="20%">🎭<br/><b>Stories</b><br/><sub>Multi-voice script editor</sub></td>
<td align="center" width="20%">⌨️<br/><b>Dictation Widget</b><br/><sub><kbd>⌘⇧Space</kbd> in any app</sub></td>
<td align="center" width="20%">🔐<br/><b>100% Local</b><br/><sub>No keys, no cloud, no accounts</sub></td>
<td align="center" width="20%">🔐<br/><b>Local-first</b><br/><sub>Core creation stays on your machine</sub></td>
<td align="center" width="20%">🤖<br/><b>MCP Server</b><br/><sub>Use from Claude, Cursor, …</sub></td>
</tr>
</table>
@@ -178,22 +133,22 @@ Three flagships, five more headliners, and a dozen under the fold.
---
<a id="why-ovs"></a>
<a id="why-voicestudio"></a>
## ⚖️ vs Others
## ⚖️ Why VoiceStudio
ElevenLabs charges **$5$330/mo** and processes your audio on their servers. VoiceStudio runs **on your hardware, with no usage limits.**
Cloud voice tools are convenient, but they put your workflow behind an account, a meter, and somebody else's infrastructure. VoiceStudio gives you a capable studio that runs on your hardware, with optional integrations when you choose them.
| | **ElevenLabs** | **VoiceStudio** |
|---|---|---|
| **Pricing** | $5$330/mo, per-character billing | Free & open-source (AGPL-3.0) · [Commercial license](#license) for proprietary use |
| **Pricing** | Subscription and usage limits | Free & open-source (AGPL-3.0) · [Commercial license](#license) for proprietary use |
| **Voice Cloning** | ✅ 3s clip | ✅ 3s clip, zero-shot |
| **Voice Design** | ✅ Gender, age | ✅ Gender, age, accent, pitch, style, dialect |
| **Audiobook / Stories** | ❌ | ✅ Full audiobook editor + multi-voice stories (EPUB/PDF import, .m4b export) |
| **Languages** | 32 | **646** |
| **Languages** | Plan/model dependent | **646** |
| **Video Dubbing** | ✅ Cloud-only | ✅ Fully local |
| **Data Privacy** | Audio sent to cloud | **Nothing leaves your machine** |
| **API Keys** | Required | Not needed |
| **Data Privacy** | Audio is processed remotely | Core workflow runs locally; online services are explicit opt-ins |
| **API Keys** | Account required | Not needed for the local workflow |
| **GPU Support** | N/A (cloud) | CUDA · Apple Silicon · ROCm (Linux) · CPU |
| **Desktop App** | ❌ | ✅ macOS · Windows · Linux |
| **TTS Engines** | 1 | **14** — [full matrix](#tts-engines) |
@@ -240,7 +195,7 @@ Professional-grade voice AI, minus the subscription and the cloud.
| Engine | Languages | Clone | Instruct | Linux | macOS ARM | Windows | License |
|--------|:---------:|:-----:|:--------:|:-----:|:---------:|:-------:|:-------:|
| **OmniVoice** (default) | 600+ | ✅ | ✅ | ✅ CUDA/CPU | ✅ MPS | ✅ CUDA/CPU | Built-in |
| **VoiceStudio** (default, powered by k2-fsa/OmniVoice) | 600+ | ✅ | ✅ | ✅ CUDA/CPU | ✅ MPS | ✅ CUDA/CPU | Built-in |
| **CosyVoice 3** | 9 + 18 dialects | ✅ | ✅ | ✅ CUDA/CPU | ✅ MPS | ✅ CUDA/CPU | Apache-2.0 |
| **GPT-SoVITS** | 5 | ✅ | — | ✅ CUDA/CPU | — | ✅ CUDA/CPU | MIT |
| **VoxCPM2** | 30 | ✅ | ✅ | ✅ CUDA/CPU | ✅ MPS | ✅ CUDA/CPU | Apache-2.0 |
+13 -58
View File
@@ -4,13 +4,13 @@
<img src="docs/logo.png" alt="VoiceStudio 徽标" width="120" />
<h1>VoiceStudio</h1>
<p><sub><em>原名 OmniVoice-Studio</em></sub></p>
<h3>开源版 ElevenLabs 替代品。</h3>
<p>实时听写、零样本语音克隆、电影级视频配音——全部在你的桌面上完成。<br/><b>无需账号。无需 API 密钥。无需云端。</b>一切都在你自己的设备上运行。开源,支持 <b>646 种语言</b>。</p>
<h3>创造声音,讲述故事,文件始终属于你。♡</h3>
<p>在一个开源桌面工作室里完成克隆、设计、配音、听写和有声书制作。<br/><b>默认本地优先。</b>没有订阅,也没有用量计费;联网服务始终由你主动选择。</p>
<p>
<a href="#quickstart">快速开始</a> ·
<a href="#features">功能</a> ·
<a href="#why-ovs">为什么选择 OVS</a> ·
<a href="#why-voicestudio">为什么选择 VoiceStudio</a> ·
<a href="#tts-engines">引擎</a> ·
<a href="#openai-api">API</a> ·
<a href="#sponsor--donate">捐赠</a> ·
@@ -40,56 +40,11 @@
<img src="docs/screenshot-launchpad.png" alt="VoiceStudio — 启动台" width="100%"/>
</div>
> **你的声音是你最私密的数据。为什么还要按月付费,从云端把它租回来?** 每一款主流语音工具都会把你的音频送到别人的服务器上,并按月向你收费。VoiceStudio 反其道而行:克隆、设计、配音、听写,全部在你自己的硬件上完成——646 种语言,没有计费表在转,任何数据都不离开你的设备
> **声音很私人,创作空间也应该真正属于你。** VoiceStudio 的核心流程运行在你的硬件上:克隆、设计、配音、听写,并以 646 种语言创作,不需要订阅,也没有用量计费。联网引擎和服务始终是清晰可见的可选项,而不是隐藏依赖
> [!WARNING]
> **活跃 Beta 阶段。** 各版本之间可能出现故障——如需最新修复,请从源码运行。非常欢迎 Bug 报告和 PR:[提交 Issue](https://github.com/debpalash/VoiceStudio/issues) 或 [加入 Discord](https://discord.gg/bzQavDfVV9)。
<a id="screenshots"></a>
## 📸 实际效果
<table>
<tr>
<td align="center" width="50%">
<img src="docs/screenshot-studio.png" alt="工作室" width="100%"/>
<br/><b>工作室(Studio</b><br/>
<sub>在同一个工作区里生成与克隆——3 秒音频即可复刻任何声音,646 种语言,零样本。</sub>
</td>
<td align="center" width="50%">
<img src="docs/screenshot-design.png" alt="声音设计" width="100%"/>
<br/><b>声音设计</b><br/>
<sub>从零构建新声音——性别、年龄、口音、音高、情感、方言。</sub>
</td>
</tr>
<tr>
<td align="center">
<img src="docs/screenshot-gallery.png" alt="声音库" width="100%"/>
<br/><b>声音库</b><br/>
<sub>浏览现成的原型声音,支持语言筛选——或构建你自己的声音库。</sub>
</td>
<td align="center">
<img src="docs/screenshot-dub.png" alt="视频配音" width="100%"/>
<br/><b>视频配音</b><br/>
<sub>一次端到端的真实配音:37 个片段完成转录、翻译成孟加拉语、重新配音并对齐时间轴——随时可导出为 MP4。</sub>
</td>
</tr>
<tr>
<td align="center">
<img src="docs/screenshot-engines.png" alt="设置 — 引擎" width="100%"/>
<br/><b>设置 → 引擎</b><br/>
<sub>引擎兼容性矩阵——14 个 TTS 引擎,逐引擎 GPU 预检,绝不静默回退到 CPU。</sub>
</td>
<td align="center">
<img src="docs/screenshot-settings.png" alt="设置 — 模型" width="100%"/>
<br/><b>设置 → 模型</b><br/>
<sub>一键模型商店——自动检测你的平台(CUDA / MPS / CPU)并推荐合适的模型。</sub>
</td>
</tr>
</table>
---
<a id="features"></a>
## ✨ 功能
@@ -125,8 +80,8 @@
<p>在<b>任何应用</b>中按 <kbd>⌘</kbd>+<kbd>⇧</kbd>+<kbd>Space</kbd>。<br/>转录、自动粘贴、随即消失。</p>
</td>
<td align="center" valign="top">
<h3>🔐 100% 本地</h3>
<p>无需密钥、无需云端、无需账号。<br/><b>在你的设备上</b>。</p>
<h3>🔐 本地优先</h3>
<p>核心创作流程<br/><b>在你的设备上</b>。</p>
</td>
<td align="center" valign="top">
<h3>🤖 MCP 服务器</h3>
@@ -200,22 +155,22 @@ Hugging Face Token 的配置见
---
<a id="why-ovs"></a>
<a id="why-voicestudio"></a>
## 💡 为什么选择 VoiceStudio
ElevenLabs 收费 **$5–$330/月**,并在他们的服务器上处理你的音频。VoiceStudio **在你的硬件上运行,没有任何用量限制。**
云端语音工具很方便,但工作流会依赖账号、用量计费和他人的基础设施。VoiceStudio 在你的硬件上提供完整工作室;只有你主动选择时,才会使用联网集成。
| | **ElevenLabs** | **VoiceStudio** |
|---|---|---|
| **价格** | $5$330/月,按字符计费 | 免费且开源(AGPL-3.0)· 专有用途可选 [商业许可证](#license) |
| **价格** | 订阅与用量限制 | 免费且开源(AGPL-3.0)· 专有用途可选 [商业许可证](#license) |
| **语音克隆** | ✅ 3 秒音频 | ✅ 3 秒音频,零样本 |
| **声音设计** | ✅ 性别、年龄 | ✅ 性别、年龄、口音、音高、风格、方言 |
| **有声书 / 故事** | ❌ | ✅ 完整有声书编辑器 + 多声音故事(EPUB/PDF 导入,.m4b 导出) |
| **语言** | 32 | **646** |
| **语言** | 取决于套餐和模型 | **646** |
| **视频配音** | ✅ 仅云端 | ✅ 完全本地 |
| **数据隐私** | 音频发送到云端 | **数据不离开你的设备** |
| **API 密钥** | 需要 | 不需要 |
| **数据隐私** | 音频在远端处理 | 核心流程在本地运行;联网服务必须主动选择 |
| **API 密钥** | 需要账号 | 本地流程不需要 |
| **GPU 支持** | 不适用(云端) | CUDA · Apple Silicon · ROCmLinux)· CPU |
| **桌面应用** | ❌ | ✅ macOS · Windows · Linux |
| **TTS 引擎** | 1 | **14** — [完整矩阵](#tts-engines) |
@@ -268,7 +223,7 @@ ElevenLabs 收费 **$5$330/月**,并在他们的服务器上处理你的音
| 引擎 | 语言 | 克隆 | 指令 | Linux | macOS ARM | Windows | 许可证 |
|--------|:---------:|:-----:|:--------:|:-----:|:---------:|:-------:|:-------:|
| **OmniVoice**(默认) | 600+ | ✅ | ✅ | ✅ CUDA/CPU | ✅ MPS | ✅ CUDA/CPU | 内置 |
| **VoiceStudio**(默认,由 k2-fsa/OmniVoice 驱动 | 600+ | ✅ | ✅ | ✅ CUDA/CPU | ✅ MPS | ✅ CUDA/CPU | 内置 |
| **CosyVoice 3** | 9 + 18 种方言 | ✅ | ✅ | ✅ CUDA/CPU | ✅ MPS | ✅ CUDA/CPU | Apache-2.0 |
| **GPT-SoVITS** | 5 | ✅ | — | ✅ CUDA/CPU | — | ✅ CUDA/CPU | MIT |
| **VoxCPM2** | 30 | ✅ | ✅ | ✅ CUDA/CPU | ✅ MPS | ✅ CUDA/CPU | Apache-2.0 |
+1 -1
View File
@@ -34,7 +34,7 @@ models:
# ── Required ──────────────────────────────────────────────────────────
- repo_id: "k2-fsa/OmniVoice"
label: "OmniVoice TTS (600+ languages, zero-shot)"
label: "VoiceStudio TTS (k2-fsa/OmniVoice, 600+ languages, zero-shot)"
role: TTS
size_gb: 2.4
required: true
+1 -1
View File
@@ -24,7 +24,7 @@ from pathlib import Path
# tests/test_app_version.py::test_all_version_files_in_lockstep and bumped by
# release.yml's version-bump job, so it stays equal to
# pyproject/tauri.conf/Cargo/package.json.
_FALLBACK_VERSION = "0.4.2"
_FALLBACK_VERSION = "0.5.0"
def _fallback_version() -> str:
+2 -2
View File
@@ -79,13 +79,13 @@ class Confucius4Backend(SubprocessBackend):
return False, (
"Confucius4-TTS venv not found. Set OMNIVOICE_CONFUCIUS4_TTS_DIR "
"to your Confucius4-TTS clone (the directory containing "
"requirements.txt) and restart OmniVoice. CUDA GPU recommended "
"requirements.txt) and restart VoiceStudio. CUDA GPU recommended "
"(CPU works but is slow). See docs/engines/confucius4-tts.md."
)
if not CONFUCIUS4_SIDECAR_SCRIPT.exists():
return False, (
"Confucius4-TTS sidecar script missing at "
f"{CONFUCIUS4_SIDECAR_SCRIPT} — reinstall OmniVoice."
f"{CONFUCIUS4_SIDECAR_SCRIPT} — reinstall VoiceStudio."
)
return True, "ok"
+2 -2
View File
@@ -128,7 +128,7 @@ def resolve_confucius4_venv() -> Path:
"Confucius4-TTS is not installed. Set the "
f"{_CLONE_DIR_ENV} environment variable to your Confucius4-TTS clone "
"(the directory that contains requirements.txt), then restart "
"OmniVoice. See docs/engines/confucius4-tts.md."
"VoiceStudio. See docs/engines/confucius4-tts.md."
)
cand = _bootstrap_engines_venv(Path(clone_dir))
@@ -186,7 +186,7 @@ def _bootstrap_engines_venv(clone_dir: Path) -> Path:
raise RuntimeError(
"uv is required to bootstrap the Confucius4-TTS venv but was not "
"found on PATH (and OMNIVOICE_BUNDLED_UV was not set). Install uv "
"from https://docs.astral.sh/uv/ and re-launch OmniVoice."
"from https://docs.astral.sh/uv/ and re-launch VoiceStudio."
)
logger.info(
+3 -3
View File
@@ -84,7 +84,7 @@ class DotsTTSBackend(SubprocessBackend):
if sys.platform == "win32":
return False, (
"dots.tts is not supported on Windows — upstream targets "
"Linux and macOS only. Run OmniVoice under WSL2, or use a "
"Linux and macOS only. Run VoiceStudio under WSL2, or use a "
"Linux/macOS host. See docs/engines/dots-tts.md."
)
@@ -101,13 +101,13 @@ class DotsTTSBackend(SubprocessBackend):
return False, (
"dots.tts venv not found. Set OMNIVOICE_DOTS_TTS_DIR to your "
"dots.tts clone (the directory containing pyproject.toml) and "
"restart OmniVoice. CUDA or CPU only (no MPS). See "
"restart VoiceStudio. CUDA or CPU only (no MPS). See "
"docs/engines/dots-tts.md for the full install walk-through."
)
if not DOTS_TTS_SIDECAR_SCRIPT.exists():
return False, (
"dots.tts sidecar script missing at "
f"{DOTS_TTS_SIDECAR_SCRIPT} — reinstall OmniVoice."
f"{DOTS_TTS_SIDECAR_SCRIPT} — reinstall VoiceStudio."
)
return True, "ok (CUDA when present, else CPU)"
+2 -2
View File
@@ -138,7 +138,7 @@ def resolve_dots_tts_venv() -> Path:
"dots.tts is not installed. Set the "
f"{_CLONE_DIR_ENV} environment variable to your dots.tts clone "
"(the directory that contains pyproject.toml and constraints/), "
"then restart OmniVoice. See docs/engines/dots-tts.md for the "
"then restart VoiceStudio. See docs/engines/dots-tts.md for the "
"full install walk-through."
)
@@ -192,7 +192,7 @@ def _bootstrap_engines_venv(clone_dir: Path) -> Path:
raise RuntimeError(
"uv is required to bootstrap the dots.tts venv but was not found "
"on PATH (and OMNIVOICE_BUNDLED_UV was not set). Install uv from "
"https://docs.astral.sh/uv/ and re-launch OmniVoice, or set "
"https://docs.astral.sh/uv/ and re-launch VoiceStudio, or set "
"OMNIVOICE_BUNDLED_UV to the absolute path of a uv binary."
)
+2 -2
View File
@@ -102,13 +102,13 @@ class IndexTTS2Backend(SubprocessBackend):
return False, (
"IndexTTS-2 venv not found. Set OMNIVOICE_INDEXTTS_DIR to "
"your IndexTTS clone (the directory containing checkpoints/) "
"and restart OmniVoice. See docs/engines/indextts.md for the "
"and restart VoiceStudio. See docs/engines/indextts.md for the "
"full install walk-through."
)
if not INDEXTTS_SIDECAR_SCRIPT.exists():
return False, (
"IndexTTS sidecar script missing at "
f"{INDEXTTS_SIDECAR_SCRIPT} — reinstall OmniVoice."
f"{INDEXTTS_SIDECAR_SCRIPT} — reinstall VoiceStudio."
)
return True, "ok"
+2 -2
View File
@@ -165,7 +165,7 @@ def resolve_indextts_venv() -> Path:
"IndexTTS-2 is not installed. Set the OMNIVOICE_INDEXTTS_DIR "
"environment variable to your IndexTTS clone (the directory "
"that contains checkpoints/ and pyproject.toml), then restart "
"OmniVoice. See docs/engines/indextts.md for the full install "
"VoiceStudio. See docs/engines/indextts.md for the full install "
"walk-through."
)
@@ -237,7 +237,7 @@ def _bootstrap_engines_venv(indextts_clone: Path) -> Path:
"uv is required to bootstrap the IndexTTS-2 venv but was not "
"found on PATH (and the bundled uv path was not set via the "
"OMNIVOICE_BUNDLED_UV env var). Install uv from "
"https://docs.astral.sh/uv/ and re-launch OmniVoice, or set "
"https://docs.astral.sh/uv/ and re-launch VoiceStudio, or set "
"OMNIVOICE_BUNDLED_UV to the absolute path of a uv binary."
)
+2 -2
View File
@@ -111,13 +111,13 @@ class MossTTSV15Backend(SubprocessBackend):
return False, (
"MOSS-TTS-v1.5 venv not found. Set OMNIVOICE_MOSS_TTS_V15_DIR "
"to your MOSS-TTS clone (the directory containing pyproject.toml) "
"and restart OmniVoice. CUDA or CPU only (no MPS). See "
"and restart VoiceStudio. CUDA or CPU only (no MPS). See "
"docs/engines/moss-tts-v15.md for the full install walk-through."
)
if not MOSS_TTS_V15_SIDECAR_SCRIPT.exists():
return False, (
"MOSS-TTS-v1.5 sidecar script missing at "
f"{MOSS_TTS_V15_SIDECAR_SCRIPT} — reinstall OmniVoice."
f"{MOSS_TTS_V15_SIDECAR_SCRIPT} — reinstall VoiceStudio."
)
return True, "ok (CUDA when present, else CPU)"
+2 -2
View File
@@ -160,7 +160,7 @@ def resolve_moss_tts_v15_venv() -> Path:
"MOSS-TTS-v1.5 is not installed. Set the "
f"{_CLONE_DIR_ENV} environment variable to your MOSS-TTS clone "
"(the directory that contains pyproject.toml), then restart "
"OmniVoice. See docs/engines/moss-tts-v15.md for the full "
"VoiceStudio. See docs/engines/moss-tts-v15.md for the full "
"install walk-through."
)
@@ -231,7 +231,7 @@ def _bootstrap_engines_venv(clone_dir: Path) -> Path:
raise RuntimeError(
"uv is required to bootstrap the MOSS-TTS-v1.5 venv but was not "
"found on PATH (and OMNIVOICE_BUNDLED_UV was not set). Install uv "
"from https://docs.astral.sh/uv/ and re-launch OmniVoice, or set "
"from https://docs.astral.sh/uv/ and re-launch VoiceStudio, or set "
"OMNIVOICE_BUNDLED_UV to the absolute path of a uv binary."
)
+3 -3
View File
@@ -513,7 +513,7 @@ class OmniVoiceBackend(TTSBackend):
"""
id = "omnivoice"
display_name = "OmniVoice (600 languages, zero-shot)"
display_name = "VoiceStudio (k2-fsa/OmniVoice, 600+ languages)"
gpu_compat = ("cuda", "mps", "cpu")
# Derived from the pool's own per-job budget (_GPU_VRAM_PER_JOB_GB = 5.0 in
# model_manager, itself measured from the ~1.6 GB forward + autoregressive
@@ -1785,7 +1785,7 @@ class SherpaOnnxBackend(TTSBackend):
Android / iOS
WebAssembly (browser)
This is the bridge to browser-based OmniVoice: the same engine runs natively
This is the bridge to browser-based VoiceStudio: the same engine runs natively
on desktop and compiles to WASM for the web UI.
Install: pip install sherpa-onnx
@@ -1826,7 +1826,7 @@ class SherpaOnnxBackend(TTSBackend):
return False, (
"OMNIVOICE_SHERPA_MODEL not set. Point it to a sherpa-onnx TTS "
"model directory (containing model.onnx + tokens.txt), then "
"restart OmniVoice. Download models from "
"restart VoiceStudio. Download models from "
"https://github.com/k2-fsa/sherpa-onnx/releases"
)
if not os.path.isfile(os.path.join(model_dir, "model.onnx")):
+1 -34
View File
@@ -6,7 +6,6 @@
"name": "omnivoice-studio-monorepo",
"devDependencies": {
"concurrently": "^9.2.1",
"kill-port-process": "^4.0.2",
"playwright": "^1.60.0",
"taze": "^19.14.1",
"turbo": "^2.9.18",
@@ -16,7 +15,7 @@
},
"frontend": {
"name": "omnivoice-studio",
"version": "0.3.21",
"version": "0.5.0",
"dependencies": {
"@fontsource-variable/inter": "^5.2.8",
"@fontsource-variable/source-serif-4": "^5.2.9",
@@ -602,10 +601,6 @@
"@scalar/workspace-store": ["@scalar/workspace-store@0.55.2", "", { "dependencies": { "@scalar/asyncapi-upgrader": "0.1.2", "@scalar/helpers": "0.9.0", "@scalar/json-magic": "0.12.17", "@scalar/openapi-upgrader": "0.2.9", "@scalar/schemas": "0.7.1", "@scalar/snippetz": "0.9.20", "@scalar/typebox": "0.1.3", "@scalar/types": "0.16.1", "@scalar/validation": "0.6.0", "js-base64": "^3.7.8", "type-fest": "^5.3.1", "vue": "^3.5.30", "yaml": "^2.8.3" } }, "sha512-/8BfJkave9vmweLdzH7w2TCaUFNcLu1vmE87id0QIi08enOwOExmPsz8YpUtaKLHM2bi/R2Tj/s9Uwp9i8Lttw=="],
"@sec-ant/readable-stream": ["@sec-ant/readable-stream@0.4.1", "", {}, "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg=="],
"@sindresorhus/merge-streams": ["@sindresorhus/merge-streams@4.0.0", "", {}, "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ=="],
"@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
"@swc/helpers": ["@swc/helpers@0.5.23", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-5lSsMOTXURePglDfvuAQUqkGek9Hg2kksOYay2m0+XR++b2NWYL/4sWyuvVBIs8oKnJaxkdi9whaL/sqN13afw=="],
@@ -962,8 +957,6 @@
"eventsource-parser": ["eventsource-parser@3.1.0", "", {}, "sha512-kJezFj9YFAMLeORyi7aCLxLbD5/qWMQnoMVlVPyHIll7lgRJCc3JVln9Vgl9nwQi0YkMnhdGTMNn7CkRRAptMg=="],
"execa": ["execa@9.6.1", "", { "dependencies": { "@sindresorhus/merge-streams": "^4.0.0", "cross-spawn": "^7.0.6", "figures": "^6.1.0", "get-stream": "^9.0.0", "human-signals": "^8.0.1", "is-plain-obj": "^4.1.0", "is-stream": "^4.0.1", "npm-run-path": "^6.0.0", "pretty-ms": "^9.2.0", "signal-exit": "^4.1.0", "strip-final-newline": "^4.0.0", "yoctocolors": "^2.1.1" } }, "sha512-9Be3ZoN4LmYR90tUoVu2te2BsbzHfhJyfEiAVfz7N5/zv+jduIfLrV2xdQXOHbaD6KgpGdO9PRPM1Y4Q9QkPkA=="],
"expect-type": ["expect-type@1.3.0", "", {}, "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA=="],
"extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="],
@@ -980,8 +973,6 @@
"fflate": ["fflate@0.4.8", "", {}, "sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA=="],
"figures": ["figures@6.1.0", "", { "dependencies": { "is-unicode-supported": "^2.0.0" } }, "sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg=="],
"file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="],
"find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="],
@@ -1020,10 +1011,6 @@
"get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="],
"get-stream": ["get-stream@9.0.1", "", { "dependencies": { "@sec-ant/readable-stream": "^0.4.1", "is-stream": "^4.0.1" } }, "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA=="],
"get-them-args": ["get-them-args@1.3.2", "", {}, "sha512-LRn8Jlk+DwZE4GTlDbT3Hikd1wSHgLMme/+7ddlqKd7ldwR6LjJgTVWzBnR01wnYGe4KgrXjg287RaI22UHmAw=="],
"get-tsconfig": ["get-tsconfig@4.14.0", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA=="],
"glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="],
@@ -1098,8 +1085,6 @@
"https-proxy-agent": ["https-proxy-agent@5.0.1", "", { "dependencies": { "agent-base": "6", "debug": "4" } }, "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA=="],
"human-signals": ["human-signals@8.0.1", "", {}, "sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ=="],
"i18next": ["i18next@26.3.1", "", { "peerDependencies": { "typescript": "^5 || ^6" }, "optionalPeers": ["typescript"] }, "sha512-txQqd5EULsqEh9OJqRH15aCaOuy/nLJyhw5EHCSKLKJE1aBbb3Zve2+uQIxgWhPm1QqUQoWyQBm2kfmmIrzkcQ=="],
"i18next-browser-languagedetector": ["i18next-browser-languagedetector@8.2.1", "", { "dependencies": { "@babel/runtime": "^7.23.2" } }, "sha512-bZg8+4bdmaOiApD7N7BPT9W8MLZG+nPTOFlLiJiT8uzKXFjhxw4v2ierCXOwB5sFDMtuA5G4kgYZ0AznZxQ/cw=="],
@@ -1130,10 +1115,6 @@
"is-regexp": ["is-regexp@3.1.0", "", {}, "sha512-rbku49cWloU5bSMI+zaRaXdQHXnthP6DZ/vLnfdSKyL4zUzuWnomtOEiZZOd+ioQ+avFo/qau3KPTc7Fjy1uPA=="],
"is-stream": ["is-stream@4.0.1", "", {}, "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A=="],
"is-unicode-supported": ["is-unicode-supported@2.1.0", "", {}, "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ=="],
"isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
"jiti": ["jiti@2.7.0", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ=="],
@@ -1162,8 +1143,6 @@
"keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="],
"kill-port-process": ["kill-port-process@4.0.2", "", { "dependencies": { "get-them-args": "1.3.2", "pid-port": "2.0.1" }, "bin": { "kill-port": "dist/bin/kill-port-process.js" } }, "sha512-fO8gc45EYJQUQWozPBmdTpsR0GDvldsmrhP2I4FPoNejwyBY4Liiwj9Is7P/5rj6k07ZQ5Ob0g0k2dqQcslW/w=="],
"knip": ["knip@6.23.0", "", { "dependencies": { "fdir": "^6.5.0", "formatly": "^0.3.0", "get-tsconfig": "4.14.0", "jiti": "^2.7.0", "oxc-parser": "^0.137.0", "oxc-resolver": "11.21.3", "picomatch": "^4.0.4", "smol-toml": "^1.6.1", "strip-json-comments": "5.0.3", "tinyglobby": "^0.2.17", "unbash": "^4.0.1", "yaml": "^2.9.0", "zod": "^4.1.11" }, "bin": { "knip": "bin/knip.js", "knip-bun": "bin/knip-bun.js" } }, "sha512-2DvAOX2pZWiG4SLvRRxOAU0aWGEn1ZoVblI541xIoXFdHqq2THMZXy66/qcY5WGuW3TXhb9T1x1zd/Hd1u+yqg=="],
"levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="],
@@ -1322,8 +1301,6 @@
"node-releases": ["node-releases@2.0.37", "", {}, "sha512-1h5gKZCF+pO/o3Iqt5Jp7wc9rH3eJJ0+nh/CIoiRwjRxde/hAHyLPXYN4V3CqKAbiZPSeJFSWHmJsbkicta0Eg=="],
"npm-run-path": ["npm-run-path@6.0.0", "", { "dependencies": { "path-key": "^4.0.0", "unicorn-magic": "^0.3.0" } }, "sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA=="],
"obug": ["obug@2.1.1", "", {}, "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ=="],
"ofetch": ["ofetch@1.5.1", "", { "dependencies": { "destr": "^2.0.5", "node-fetch-native": "^1.6.7", "ufo": "^1.6.1" } }, "sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA=="],
@@ -1368,8 +1345,6 @@
"picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="],
"pid-port": ["pid-port@2.0.1", "", { "dependencies": { "execa": "^9.6.0" } }, "sha512-pnLo01AmMclw8l+/gfknsP2N351oe8VkVmCLFUvJZ11NRPPmghJrv0OcwsdgPQxsZkFYwm6hPWW0JKmXYCaXAw=="],
"playwright": ["playwright@1.60.0", "", { "dependencies": { "playwright-core": "1.60.0" }, "optionalDependencies": { "fsevents": "2.3.2" }, "bin": { "playwright": "cli.js" } }, "sha512-hheHdokM8cdqCb0lcE3s+zT4t4W+vvjpGxsZlDnikarzx8tSzMebh3UiFtgqwFwnTnjYQcsyMF8ei2mCO/tpeA=="],
"playwright-core": ["playwright-core@1.61.0", "", { "bin": { "playwright-core": "cli.js" } }, "sha512-caX7TrY3Ml6egyDX0WUcTHDxodl/b51y5wJOdCEA36QviK/s2g081hvmGs8eaE3DWb6NYZQ6BjO/QkNRPenoPA=="],
@@ -1502,8 +1477,6 @@
"strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
"strip-final-newline": ["strip-final-newline@4.0.0", "", {}, "sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw=="],
"strip-indent": ["strip-indent@3.0.0", "", { "dependencies": { "min-indent": "^1.0.0" } }, "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ=="],
"strip-json-comments": ["strip-json-comments@5.0.3", "", {}, "sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw=="],
@@ -1584,8 +1557,6 @@
"unhead": ["unhead@2.1.15", "", { "dependencies": { "hookable": "^6.0.1" } }, "sha512-MCt5T90mCWyr3Z6pUCdM9lVRXoMoVBlL7z7U4CYVIiaDiuzad/UCfLuMqz5MeNmpZUgoBCQnrucJimU7EZR+XA=="],
"unicorn-magic": ["unicorn-magic@0.3.0", "", {}, "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA=="],
"unified": ["unified@11.0.5", "", { "dependencies": { "@types/unist": "^3.0.0", "bail": "^2.0.0", "devlop": "^1.0.0", "extend": "^3.0.0", "is-plain-obj": "^4.0.0", "trough": "^2.0.0", "vfile": "^6.0.0" } }, "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA=="],
"unist-util-find-after": ["unist-util-find-after@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-amQa0Ep2m6hE2g72AugUItjbuM8X8cGQnFoHk0pGfrFeT9GZhzN5SW8nRsiGKK7Aif4CrACPENkA6P/Lw6fHGQ=="],
@@ -1678,8 +1649,6 @@
"yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="],
"yoctocolors": ["yoctocolors@2.1.2", "", {}, "sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug=="],
"zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="],
"zod-validation-error": ["zod-validation-error@4.0.2", "", { "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } }, "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ=="],
@@ -1766,8 +1735,6 @@
"mdast-util-find-and-replace/escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="],
"npm-run-path/path-key": ["path-key@4.0.0", "", {}, "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ=="],
"playwright/playwright-core": ["playwright-core@1.60.0", "", { "bin": { "playwright-core": "cli.js" } }, "sha512-9bW6zvX/m0lEbgTKJ6YppOKx8H3VOPBMOCFh2irXFOT4BbHgrx5hPjwJYLT40Lu+4qtD36qKc/Hn56StUW57IA=="],
"pretty-format/ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="],
+2 -2
View File
@@ -13,9 +13,9 @@
# Open http://localhost:3900 once the health check passes.
#
# SECURITY: The port is bound to 127.0.0.1 by default — only this
# machine can reach the API. To expose OmniVoice on your LAN (or
# machine can reach the API. To expose VoiceStudio on your LAN (or
# through a reverse proxy / tunnel), change the port mapping to
# "0.0.0.0:3900:3900" or "3900:3900". OmniVoice itself ships no
# "0.0.0.0:3900:3900" or "3900:3900". VoiceStudio itself ships no
# authentication — if you expose it, put it behind a reverse proxy
# with auth (Caddy basic_auth, nginx + htpasswd, Tailscale, etc.).
# ──────────────────────────────────────────────────────────────
+2 -2
View File
@@ -18,7 +18,7 @@ features:
- Batch Queue
- MCP Server
- AI Watermark
- 100% Local
- Local-first
- GPU Auto-Detect
- Extensible
@@ -28,7 +28,7 @@ features:
# doc (optional): a repo-relative doc file that must exist.
tts_engines:
- id: omnivoice
readme: "**OmniVoice** (default)"
readme: "**VoiceStudio** (default, powered by k2-fsa/OmniVoice)"
- id: omnivoice-subprocess
doc: docs/engines/omnivoice-subprocess.md
- id: cosyvoice
Binary file not shown.

Before

Width:  |  Height:  |  Size: 426 KiB

After

Width:  |  Height:  |  Size: 273 KiB

+8 -8
View File
@@ -1,17 +1,17 @@
"""Minimal pipecat agent that speaks and listens through local OmniVoice.
"""Minimal pipecat agent that speaks and listens through local VoiceStudio.
OmniVoice is used purely as an OpenAI-compatible TTS/STT provider nothing
VoiceStudio is used purely as an OpenAI-compatible TTS/STT provider nothing
leaves your machine. See docs/agentic-voice.md for the full recipe.
Run OmniVoice first (default http://localhost:3900), then:
Run VoiceStudio first (default http://localhost:3900), then:
uv pip install "pipecat-ai[openai,silero]"
python examples/agentic/pipecat_minimal.py
This is a deliberately tiny skeleton: it wires the OmniVoice TTS/STT services
This is a deliberately tiny skeleton: it wires the VoiceStudio TTS/STT services
into a pipecat pipeline and leaves the transport + LLM for you to choose. It
does not run a phone call or a server that is the "agentic v1" scope
(OmniVoice as provider, you bring the runtime).
(VoiceStudio as provider, you bring the runtime).
"""
from __future__ import annotations
@@ -19,7 +19,7 @@ from __future__ import annotations
import os
OMNIVOICE_BASE_URL = os.environ.get("OMNIVOICE_API_URL", "http://localhost:3900") + "/v1"
# OmniVoice ignores the key for local use; if you set OMNIVOICE_API_KEY on a
# VoiceStudio ignores the key for local use; if you set OMNIVOICE_API_KEY on a
# remote backend, pass that same value here.
OMNIVOICE_API_KEY = os.environ.get("OMNIVOICE_API_KEY", "not-needed-locally")
# A voice-profile id from GET /v1/audio/voices, or "default".
@@ -27,7 +27,7 @@ OMNIVOICE_VOICE = os.environ.get("OMNIVOICE_VOICE", "default")
def build_services():
"""Return (stt, tts) backed by local OmniVoice.
"""Return (stt, tts) backed by local VoiceStudio.
Imported lazily so this file is importable (and lint-clean) without
pipecat installed the smoke test in CI checks the wiring shape, not a
@@ -52,7 +52,7 @@ def build_services():
def main() -> None:
stt, tts = build_services()
print("OmniVoice STT + TTS services constructed against", OMNIVOICE_BASE_URL)
print("VoiceStudio STT + TTS services constructed against", OMNIVOICE_BASE_URL)
print("Wire `stt` and `tts` into your pipecat Pipeline with a transport")
print("and an LLM service. See docs/agentic-voice.md.")
+3 -3
View File
@@ -1,10 +1,10 @@
import { test, expect } from '@playwright/test';
import { collectErrors, gotoMode } from './_helpers';
test.describe('OmniVoice Gallery', () => {
test('heading is "OmniVoice Gallery"', async ({ page }) => {
test.describe('VoiceStudio Gallery', () => {
test('heading is "VoiceStudio Gallery"', async ({ page }) => {
await gotoMode(page, 'gallery');
await expect(page.getByRole('heading', { name: /OmniVoice Gallery/i })).toBeVisible();
await expect(page.getByRole('heading', { name: /VoiceStudio Gallery/i })).toBeVisible();
});
test('facet dropdowns use the dark theme, not the OS-default light surface', async ({ page }) => {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "omnivoice-studio",
"version": "0.4.2",
"version": "0.5.0",
"private": true,
"license": "AGPL-3.0-only",
"type": "module",
+1 -1
View File
@@ -2941,7 +2941,7 @@ dependencies = [
[[package]]
name = "omnivoice-studio"
version = "0.4.2"
version = "0.5.0"
dependencies = [
"arboard",
"dirs-next",
+1 -1
View File
@@ -4,7 +4,7 @@
# launcher's pkill matches `omnivoice-studio` and must never match a user's
# installed app. Renaming it would collapse that distinction.
name = "omnivoice-studio"
version = "0.4.2"
version = "0.5.0"
description = "VoiceStudio AI voice cloning & dubbing desktop app"
authors = ["Debpalash"]
license = "AGPL-3.0-only"
+2 -2
View File
@@ -10,7 +10,7 @@
into the app's Info.plist by tauri-bundler at bundle time.
-->
<key>NSMicrophoneUsageDescription</key>
<string>OmniVoice needs microphone access for live dictation and voice recording. Audio is processed entirely on your machine — nothing is sent to any external server.</string>
<string>VoiceStudio needs microphone access for live dictation and voice recording. Audio is processed on your machine.</string>
<!--
Same story for camera. We don't currently use it, but if a future
@@ -18,6 +18,6 @@
need this string. Cheap to ship now; avoids a future TCC denial.
-->
<key>NSCameraUsageDescription</key>
<string>OmniVoice may use the camera for upcoming video features. Video stays on your machine.</string>
<string>VoiceStudio may use the camera for upcoming video features. Video stays on your machine.</string>
</dict>
</plist>
+1 -1
View File
@@ -301,7 +301,7 @@ if _prefer_system_gstreamer; then
# Fail safe, and say so: the app still starts on the bundled core, which
# is where the microphone problem lives, so the user needs a thread to
# pull rather than a silent half-fix.
echo "OmniVoice: your GStreamer (${_SYS_GST_LIB}) cannot load against the" >&2
echo "VoiceStudio: your GStreamer (${_SYS_GST_LIB}) cannot load against the" >&2
echo " libraries this AppImage bundles, so it is not being used. Audio" >&2
echo " capture may not find any microphone (see issue #1333). Building" >&2
echo " from source avoids the mismatch entirely." >&2
+1 -1
View File
@@ -298,7 +298,7 @@ export default function Header({
<path d="M16 9v6" />
</svg>
<span className="text-[0.92rem] font-semibold text-[var(--chrome-fg)] tracking-[0.02em] [font-family:var(--font-sans)] not-italic">
Omni<span className="text-[var(--chrome-accent)]">Voice</span>
Voice<span className="text-[var(--chrome-accent)]">Studio</span>
</span>
</div>
)}
+3 -3
View File
@@ -61,9 +61,9 @@ export default function NavRail({ mode, setMode, side = 'left', onFlipSide }) {
return (
<aside
className={`nav-rail z-50 flex select-none flex-col items-center gap-[6px] bg-[var(--chrome-bg)] py-[8px] ${asideBorder}`}
className={`nav-rail z-50 flex select-none flex-col items-center gap-[10px] bg-[var(--chrome-bg)] pb-[10px] pt-[18px] ${asideBorder}`}
>
<div className="flex flex-1 flex-col items-center gap-[4px]">
<div className="flex flex-1 flex-col items-center gap-[9px]">
{items.map((it) => (
<RailBtn
key={it.id}
@@ -74,7 +74,7 @@ export default function NavRail({ mode, setMode, side = 'left', onFlipSide }) {
/>
))}
</div>
<div className="flex flex-col items-center gap-[4px]">
<div className="flex flex-col items-center gap-[8px]">
{footerItems.map((it) => (
<RailBtn
key={it.id}
+2 -2
View File
@@ -30,7 +30,7 @@ describe('Header — rail mode (default)', () => {
expect(container.querySelector('.header-area--tabs')).toBeNull();
expect(screen.queryByTestId('titletab-dub')).toBeNull();
// Breadcrumb (current view) + centred wordmark both stay.
expect(container.textContent).toMatch(/OmniVoice/);
expect(container.textContent).toMatch(/VoiceStudio/);
expect(container.textContent).toMatch(/Dub/);
});
});
@@ -45,6 +45,6 @@ describe('Header — titlebar tabs mode', () => {
it('drops the centred wordmark — the tabs need that room', () => {
const { container } = renderHeader({ navStyle: 'tabs' });
expect(container.textContent).not.toMatch(/OmniVoice/);
expect(container.textContent).not.toMatch(/VoiceStudio/);
});
});
@@ -81,6 +81,15 @@ describe('NavRail — same list, other skin', () => {
expect(screen.getByRole('button', { name: i18n.t(`nav.${item.tKey}`) })).toBeInTheDocument();
}
});
it('keeps the compact icon rail airy below the titlebar', () => {
const { container } = render(
<NavRail mode="launchpad" setMode={() => {}} side="left" onFlipSide={() => {}} />,
);
const rail = container.querySelector('.nav-rail');
expect(rail.className).toContain('pt-[18px]');
expect(rail.firstElementChild.className).toContain('gap-[9px]');
});
});
describe('appShellClasses — the grid only reserves what renders', () => {
+1 -1
View File
@@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>OmniVoice — Visual Regression Harness</title>
<title>VoiceStudio — Visual Regression Harness</title>
</head>
<body style="margin: 0; background: #1d2021">
<!-- Isolated component-snapshot harness. NOT part of the shipped app
+2 -3
View File
@@ -11,9 +11,9 @@
"dev:desktop": "TAURI_SKIP_BACKEND=1 bun run --cwd frontend desktop",
"wait:api": "wait-on -t 300000 http-get://localhost:3900/system/info",
"wait:frontend": "wait-on -t 120000 http://localhost:5173",
"predev": "bun install && (kill-port 3900 3901 || true)",
"predev": "bun install && bun scripts/clear-dev-ports.mjs 3900 3901",
"dev": "bun run setup:api && concurrently -n api,fe -c green,cyan --kill-others-on-fail \"bun run dev:api\" \"bun run dev:frontend\"",
"predesktop": "bun install && (kill-port 3900 3901 || true)",
"predesktop": "bun install && bun scripts/clear-dev-ports.mjs 3900 3901",
"desktop": "bun run setup:api && concurrently -n api,app -c green,magenta --kill-others-on-fail \"bun run dev:api\" \"bun run dev:desktop\"",
"desktop-prod": "bun scripts/desktop-prod.mjs",
"desktop-prod:run": "bun scripts/desktop-prod.mjs --skip-build --keep-data",
@@ -36,7 +36,6 @@
],
"devDependencies": {
"concurrently": "^9.2.1",
"kill-port-process": "^4.0.2",
"playwright": "^1.60.0",
"taze": "^19.14.1",
"turbo": "^2.9.18",
+2 -2
View File
@@ -4,8 +4,8 @@ build-backend = "hatchling.build"
[project]
name = "omnivoice"
version = "0.4.2"
description = "OmniVoice: Towards Omnilingual Zero-Shot Text-to-Speech with Diffusion Language Models"
version = "0.5.0"
description = "VoiceStudio — a private, local-first studio for voice cloning, speech generation, dubbing, transcription, and audiobooks"
readme = "README.md"
# Free and open-source under the GNU Affero General Public License v3 (see
# LICENSE). A commercial license is available for proprietary/closed-source use
+209
View File
@@ -0,0 +1,209 @@
#!/usr/bin/env bun
import { spawnSync } from "node:child_process";
import { readFileSync, readlinkSync } from "node:fs";
import { dirname, resolve, sep } from "node:path";
import { fileURLToPath } from "node:url";
const DEFAULT_PORTS = [3900, 3901];
const CHECKOUT_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");
export function parseWindowsListeners(output, ports) {
const wanted = new Set(ports);
const pids = new Set();
for (const line of output.split(/\r?\n/)) {
const match = line.match(/^\s*TCP\s+\S+:(\d+)\s+\S+\s+LISTENING\s+(\d+)\s*$/i);
if (match && wanted.has(Number(match[1]))) pids.add(Number(match[2]));
}
return [...pids];
}
export function parseSsListeners(output, ports) {
const wanted = new Set(ports);
const pids = new Set();
for (const line of output.split(/\r?\n/)) {
const portMatch = line.match(/\]?:([0-9]+)\s/);
if (!portMatch || !wanted.has(Number(portMatch[1]))) continue;
for (const match of line.matchAll(/pid=(\d+)/g)) pids.add(Number(match[1]));
}
return [...pids];
}
function validPid(pid) {
return Number.isInteger(pid) && pid > 1 && pid !== process.pid;
}
function unixListeners(ports) {
const pids = new Set();
for (const port of ports) {
const result = spawnSync("lsof", ["-nP", "-t", `-iTCP:${port}`, "-sTCP:LISTEN"], {
encoding: "utf8",
});
if (!result.error) {
for (const value of result.stdout.split(/\s+/)) {
const pid = Number(value);
if (validPid(pid)) pids.add(pid);
}
continue;
}
if (result.error.code !== "ENOENT") throw result.error;
const fallback = spawnSync("ss", ["-ltnp"], { encoding: "utf8" });
if (fallback.error) throw fallback.error;
for (const pid of parseSsListeners(fallback.stdout, ports)) {
if (validPid(pid)) pids.add(pid);
}
break;
}
return [...pids];
}
function windowsListeners(ports) {
const result = spawnSync("netstat", ["-ano", "-p", "tcp"], { encoding: "utf8" });
if (result.error) throw result.error;
return parseWindowsListeners(result.stdout, ports).filter(validPid);
}
function normalized(value, windows = process.platform === "win32") {
const text = resolve(String(value || ""));
return windows ? text.toLowerCase() : text;
}
function belongsToCheckout(cwd, command, executable, windows = process.platform === "win32") {
const root = normalized(CHECKOUT_ROOT, windows);
const prefix = `${root}${sep}`;
const ownedPath = (value) => {
if (!value) return false;
const path = normalized(value, windows);
return path === root || path.startsWith(prefix);
};
if (ownedPath(cwd) || ownedPath(executable)) return true;
const haystack = windows ? String(command || "").toLowerCase() : String(command || "");
return haystack.includes(root);
}
function inspectLinux(pid) {
try {
const cwd = readlinkSync(`/proc/${pid}/cwd`);
const command = readFileSync(`/proc/${pid}/cmdline`, "utf8").replaceAll("\0", " ");
const stat = readFileSync(`/proc/${pid}/stat`, "utf8");
const afterName = stat.slice(stat.lastIndexOf(")") + 2).trim().split(/\s+/);
const startTime = afterName[19]; // proc(5): field 22; this array starts at field 3.
if (!startTime) return null;
return {
identity: `linux:${startTime}`,
owned: belongsToCheckout(cwd, command, "", false),
};
} catch (error) {
if (error?.code === "ENOENT" || error?.code === "ESRCH") return null;
throw error;
}
}
function inspectMac(pid) {
const cwdResult = spawnSync("lsof", ["-a", "-p", String(pid), "-d", "cwd", "-Fn"], {
encoding: "utf8",
});
const startResult = spawnSync("ps", ["-p", String(pid), "-o", "lstart="], { encoding: "utf8" });
const commandResult = spawnSync("ps", ["-p", String(pid), "-o", "command="], {
encoding: "utf8",
});
if (startResult.status !== 0 || !startResult.stdout.trim()) return null;
if (cwdResult.error) throw cwdResult.error;
if (commandResult.error) throw commandResult.error;
const cwdLine = cwdResult.stdout.split(/\r?\n/).find((line) => line.startsWith("n"));
const cwd = cwdLine?.slice(1) || "";
return {
identity: `mac:${startResult.stdout.trim()}`,
owned: belongsToCheckout(cwd, commandResult.stdout.trim(), "", false),
};
}
function inspectWindows(pid) {
const script = [
`$p = Get-CimInstance Win32_Process -Filter 'ProcessId = ${pid}'`,
"if ($null -ne $p) {",
" $p | Select-Object ProcessId,ExecutablePath,CommandLine,CreationDate | ConvertTo-Json -Compress",
"}",
].join("; ");
const result = spawnSync("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", script], {
encoding: "utf8",
});
if (result.error) throw result.error;
if (result.status !== 0) throw new Error(`Could not inspect process ${pid}`);
if (!result.stdout.trim()) return null;
const info = JSON.parse(result.stdout);
return {
identity: `windows:${info.CreationDate}`,
owned: belongsToCheckout("", info.CommandLine, info.ExecutablePath, true),
};
}
function systemOperations() {
const windows = process.platform === "win32";
return {
listeners: windows ? windowsListeners : unixListeners,
inspect: windows ? inspectWindows : process.platform === "darwin" ? inspectMac : inspectLinux,
stop(pid, force) {
if (windows) {
const args = force
? ["/F", "/T", "/PID", String(pid)]
: ["/T", "/PID", String(pid)];
const result = spawnSync("taskkill", args, { stdio: "ignore" });
if (result.status !== 0) throw new Error(`Could not stop process ${pid}`);
} else {
process.kill(pid, force ? "SIGKILL" : "SIGTERM");
}
},
sleep(ms) {
return new Promise((done) => setTimeout(done, ms));
},
};
}
async function inspectSameProcess(ops, pid, expectedIdentity) {
const current = await ops.inspect(pid);
if (!current || current.identity !== expectedIdentity) return null;
if (!current.owned) throw new Error(`Refusing to stop unrelated process ${pid}`);
return current;
}
export async function clearDevPortsWith(ports, ops) {
const listeners = await ops.listeners(ports);
for (const pid of listeners) {
const first = await ops.inspect(pid);
if (!first) continue;
if (!first.owned) throw new Error(`Refusing to stop unrelated process ${pid}`);
if (!(await inspectSameProcess(ops, pid, first.identity))) continue;
await ops.stop(pid, false);
let current = first;
for (let attempt = 0; attempt < 10; attempt += 1) {
await ops.sleep(50);
current = await inspectSameProcess(ops, pid, first.identity);
if (!current) break;
}
if (!current) continue;
// Revalidate immediately before escalation. A recycled PID is never killed.
if (!(await inspectSameProcess(ops, pid, first.identity))) continue;
await ops.stop(pid, true);
}
const remaining = await ops.listeners(ports);
if (remaining.length) throw new Error(`Ports still occupied by process ${remaining.join(", ")}`);
}
export async function clearDevPorts(ports = DEFAULT_PORTS) {
const uniquePorts = [...new Set(ports.map(Number))];
if (uniquePorts.some((port) => !Number.isInteger(port) || port < 1 || port > 65535)) {
throw new TypeError(`Invalid port list: ${ports.join(", ")}`);
}
return clearDevPortsWith(uniquePorts, systemOperations());
}
if (import.meta.main) {
const ports = process.argv.slice(2).length ? process.argv.slice(2).map(Number) : DEFAULT_PORTS;
await clearDevPorts(ports);
}
+75
View File
@@ -0,0 +1,75 @@
import assert from "node:assert/strict";
import test from "node:test";
import {
clearDevPortsWith,
parseSsListeners,
parseWindowsListeners,
} from "../../scripts/clear-dev-ports.mjs";
test("parses only requested Windows TCP listeners", () => {
const output = [
" TCP 0.0.0.0:3900 0.0.0.0:0 LISTENING 1234",
" TCP [::]:3901 [::]:0 LISTENING 5678",
" TCP 0.0.0.0:5173 0.0.0.0:0 LISTENING 9999",
].join("\r\n");
assert.deepEqual(parseWindowsListeners(output, [3900, 3901]), [1234, 5678]);
});
test("parses only requested Linux listeners and deduplicates pids", () => {
const output = [
'LISTEN 0 512 *:3900 *:* users:(("bun",pid=1234,fd=11))',
'LISTEN 0 512 127.0.0.1:3901 0.0.0.0:* users:(("bun",pid=1234,fd=12))',
'LISTEN 0 512 *:5173 *:* users:(("bun",pid=9999,fd=8))',
].join("\n");
assert.deepEqual(parseSsListeners(output, [3900, 3901]), [1234]);
});
test("refuses an unrelated listener without signalling it", async () => {
const signals = [];
const ops = {
listeners: async () => [1234],
inspect: async () => ({ identity: "start-a", owned: false }),
stop: async (...args) => signals.push(args),
sleep: async () => {},
};
await assert.rejects(
clearDevPortsWith([3900], ops),
/Refusing to stop unrelated process 1234/,
);
assert.deepEqual(signals, []);
});
test("never force-kills a recycled pid", async () => {
const signals = [];
let inspections = 0;
let discovery = 0;
const ops = {
listeners: async () => (discovery++ === 0 ? [1234] : []),
inspect: async () => {
inspections += 1;
if (inspections <= 3) return { identity: "start-a", owned: true };
return { identity: "start-b", owned: false };
},
stop: async (pid, force) => signals.push([pid, force]),
sleep: async () => {},
};
await clearDevPortsWith([3900], ops);
assert.deepEqual(signals, [[1234, false]]);
});
test("escalates only while ownership and identity remain stable", async () => {
const signals = [];
let discovery = 0;
const ops = {
listeners: async () => (discovery++ === 0 ? [1234] : []),
inspect: async () => ({ identity: "start-a", owned: true }),
stop: async (pid, force) => signals.push([pid, force]),
sleep: async () => {},
};
await clearDevPortsWith([3900], ops);
assert.deepEqual(signals, [
[1234, false],
[1234, true],
]);
});
+80
View File
@@ -0,0 +1,80 @@
"""VoiceStudio 0.5.0 release-brand and source-launch contracts."""
from __future__ import annotations
import json
import re
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
def test_release_version_is_0_5_0_everywhere() -> None:
package = json.loads((ROOT / "frontend/package.json").read_text())
assert package["version"] == "0.5.0"
mirrors = {
"pyproject.toml": r'(?m)^version = "([^"]+)"',
"frontend/src-tauri/Cargo.toml": r'(?m)^version = "([^"]+)"',
"backend/core/version.py": r'(?m)^_FALLBACK_VERSION = "([^"]+)"',
}
for path, pattern in mirrors.items():
match = re.search(pattern, (ROOT / path).read_text())
assert match and match.group(1) == "0.5.0", path
def test_visible_brand_surfaces_say_voicestudio() -> None:
header = (ROOT / "frontend/src/components/Header.jsx").read_text()
assert "Voice<span" in header and ">Studio</span>" in header
assert "Omni<span" not in header
visible_files = (
"frontend/src-tauri/Info.plist",
"frontend/src-tauri/appimage/AppRun",
"frontend/src/test/visual/harness.html",
"frontend/e2e/gallery.spec.ts",
)
for path in visible_files:
text = (ROOT / path).read_text()
assert "VoiceStudio" in text, path
assert "OmniVoice needs" not in text, path
assert "OmniVoice may" not in text, path
assert "OmniVoice Gallery" not in text, path
readme = (ROOT / "README.md").read_text()
assert "**VoiceStudio** (default, powered by k2-fsa/OmniVoice)" in readme
assert "**OmniVoice** (default)" not in readme
def test_engine_help_names_the_app_not_the_upstream_model() -> None:
paths = (
"backend/engines/confucius4/__init__.py",
"backend/engines/confucius4/bootstrap.py",
"backend/engines/dots_tts/__init__.py",
"backend/engines/dots_tts/bootstrap.py",
"backend/engines/indextts/__init__.py",
"backend/engines/indextts/bootstrap.py",
"backend/engines/moss_tts_v15/__init__.py",
"backend/engines/moss_tts_v15/bootstrap.py",
)
stale_help = re.compile(r"(?:restart|reinstall|re-launch|Run) OmniVoice")
for path in paths:
text = (ROOT / path).read_text()
assert not stale_help.search(text), path
def test_source_launch_cleans_idle_ports_quietly() -> None:
scripts = json.loads((ROOT / "package.json").read_text())["scripts"]
for name in ("predev", "predesktop"):
command = scripts[name]
assert "bun scripts/clear-dev-ports.mjs 3900 3901" in command
assert "|| true" not in command
def test_icon_rail_has_no_static_section_captions_and_keeps_air_between_items() -> None:
rail = (ROOT / "frontend/src/components/NavRail.jsx").read_text()
for stale_caption in ("Start", "Create", "Workflows", "Reference"):
assert stale_caption not in rail
assert "pt-[18px]" in rail
assert "gap-[9px]" in rail
Generated
+1 -1
View File
@@ -3253,7 +3253,7 @@ wheels = [
[[package]]
name = "omnivoice"
version = "0.4.2"
version = "0.5.0"
source = { editable = "." }
dependencies = [
{ name = "accelerate" },