Release VoiceStudio 5.0.0 (#1487)

Complete the VoiceStudio identity, release documentation, assets, version mirrors, and safe cross-platform development startup.
This commit is contained in:
Palash Debnath
2026-08-11 04:05:45 +00:00
committed by GitHub
parent 3df44486b2
commit 04410a458d
94 changed files with 793 additions and 339 deletions
+1
View File
@@ -202,6 +202,7 @@ class MyEngineBackend(TTSBackend):
- **Components**: Functional components with hooks
- **State**: Zustand stores in `src/stores/`, organized by slice
- **Brand assets**: Reuse the canonical mark, palette, naming, and compatibility rules in [`docs/branding.md`](../docs/branding.md); do not redraw or rename runtime identifiers ad hoc
- **CSS**: **Utilities-first + shadcn/ui, one stylesheet.** UI is built on the shadcn/ui primitives in `src/components/ui/` (wrapped by the `src/ui/` barrel, themed to the VoiceStudio palette), composed with Tailwind v4 utility classes. **All styling now lives in a single file — `src/index.css`**: the `@theme` / `[data-theme]` token foundation plus the irreducible set utilities can't express (`@keyframes`, glassmorphism/`backdrop-filter`, pseudo-elements, `:has()`, unlayered cascade overrides, and styling hooks on library-generated DOM like virtualized rows / WaveSurfer). The per-component `.css` files were eliminated in the CSS→Tailwind/shadcn migration — **do not create new ones.** Reach for shadcn primitives + utilities; if a rule is genuinely irreducible, add it to `src/index.css` with a provenance comment. (The only other `.css` is the test-only visual harness. See `docs/shadcn-migration.md`.)
- **Naming**: `PascalCase` for components, `camelCase` for hooks and utils
+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.
+9 -2
View File
@@ -3,13 +3,19 @@
All notable changes to VoiceStudio.
The format is loosely based on [Keep a Changelog](https://keepachangelog.com/).
Versions track the desktop app (`tauri.conf.json` + `frontend/src-tauri/Cargo.toml`).
The bundled TTS model package (`pyproject.toml`) is versioned independently.
`frontend/package.json` is the app-version source of truth; Cargo, Python, and
the frozen-backend fallback mirror it for their toolchains.
## [Unreleased]
## [5.0.0] — 2026-08-11
**Highlights**
- VoiceStudio 5.0.0 gives the app, desktop chrome, documentation, and package metadata one clear identity
- 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
@@ -21,6 +27,7 @@ The bundled TTS model package (`pyproject.toml`) is versioned independently.
### Changed
- VoiceStudio now uses one waveform-and-spark mark across the title bar, About screen, README, browser favicon, and every desktop/platform icon. (#1487)
- PocketTTS now asks you to review its code license, model license and gated-access conditions before first use, and explains how to unlock the model instead of showing a raw download failure — thanks @paoloantinori! (#1442)
- The repository moved to github.com/debpalash/VoiceStudio. Every link in the app, docs and scripts now points there; GitHub redirects the old URLs, and the Docker image paths, the app bundle identifier and your data folder are all deliberately unchanged. (#1394)
- The app is now **VoiceStudio** (previously OmniVoice-Studio). Only the name you see changes — your data folder, settings and the Docker image paths stay put, so upgrading needs nothing from you. On Linux the .deb is now `voicestudio`; remove the old `omnivoice-studio` package once.
+14 -59
View File
@@ -1,14 +1,14 @@
<div align="center">
<img src="docs/logo.png" alt="VoiceStudio Logo" width="120" />
<img src="docs/logo.png" alt="VoiceStudio Logo" width="120" height="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 |
+14 -59
View File
@@ -1,16 +1,16 @@
*本文档是 [README.md](README.md) 的简体中文翻译;若与英文版有出入,以英文版为准。*
<div align="center">
<img src="docs/logo.png" alt="VoiceStudio 徽标" width="120" />
<img src="docs/logo.png" alt="VoiceStudio 徽标" width="120" height="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
@@ -1,5 +1,5 @@
<div align="center">
<img src="docs/logo.png" alt="VoiceStudio Logo" width="96" />
<img src="docs/logo.png" alt="VoiceStudio Logo" width="96" height="96" />
<h1>Sponsor VoiceStudio</h1>
<p><b>Keep the open-source ElevenLabs alternative free, local, and shipping.</b></p>
</div>
+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 = "5.0.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
@@ -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": "5.0.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.).
# ──────────────────────────────────────────────────────────────
+61
View File
@@ -0,0 +1,61 @@
# VoiceStudio brand
VoiceStudio uses a waveform-and-spark mark. The waveform identifies audio; the
spark identifies creation. The product voice is clear, calm, and direct.
## Assets
| Surface | Source |
|---|---|
| Primary vector mark | `docs/logo.svg` |
| README mark | `docs/logo.png` and `docs/logo-256.png` |
| Browser icon | `frontend/public/favicon.svg` |
| In-app mark | `frontend/src/components/brand/VoiceStudioMark.jsx` |
| Desktop/platform icons | `frontend/src-tauri/icons/` |
Regenerate every desktop icon from the canonical vector after changing the
mark:
```bash
bun run --cwd frontend tauri icon ../docs/logo.svg
```
Do not redraw the waveform per screen. Use `VoiceStudioMark` in React and the
canonical SVG elsewhere so the silhouette stays recognizable at 16512 px.
## Palette
| Role | Color |
|---|---|
| Voice rose | `#F47FA3` |
| Soft highlight | `#FFD0DB` |
| Creative amber | `#F7AE73` |
| Spark | `#FFE5B5` |
| Plum tile | `#211B2B``#120F18` |
The mark may render in one color inside app chrome. Keep the waveform and spark
together; do not put text inside the icon or add another enclosing ring.
## Name and copy
- Product name: **VoiceStudio**—one word, capital V and S.
- Voice: concise, professional, warm, and direct.
- Promise: local-first creation without a subscription or usage meter.
- Avoid absolute privacy claims: network-backed engines, downloads, analytics,
and cloud integrations are explicit opt-ins, not nonexistent.
- Attribute the bundled default model as **k2-fsa/OmniVoice** where model lineage
matters. OmniVoice is an upstream model/runtime name, not the product name.
## Compatibility names
The rebrand must not break existing installations. Keep these identifiers until
a separately tested migration exists:
- `omnivoice` Python imports and package name
- `omnivoice-studio` binary/package and published container coordinates
- `OMNIVOICE_*` environment variables and `X-OmniVoice-*` API headers
- existing OmniVoice data/cache directories and uninstall aliases
- upstream repositories, model IDs, classes, and engine IDs
Visible copy can explain those compatibility names, but must not silently rename
them on disk or over the wire.
+3 -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
@@ -85,6 +85,7 @@ asr_engines:
# Doc files that must exist (the install path users are sent to).
docs:
- docs/branding.md
- docs/install/macos.md
- docs/install/windows.md
- docs/install/linux.md
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 23 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 144 KiB

After

Width:  |  Height:  |  Size: 49 KiB

+18 -56
View File
@@ -1,61 +1,23 @@
<svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" viewBox="0 0 512 512">
<svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" viewBox="0 0 64 64" role="img" aria-labelledby="title description">
<title id="title">VoiceStudio</title>
<desc id="description">A warm pink speech waveform with a small spark on a dark rounded tile.</desc>
<defs>
<!-- Background gradient -->
<linearGradient id="bg" x1="0" y1="0" x2="1" y2="1">
<stop offset="0%" stop-color="#1e1e2e"/>
<stop offset="100%" stop-color="#13131f"/>
<linearGradient id="tile" x1="7" y1="4" x2="58" y2="62" gradientUnits="userSpaceOnUse">
<stop stop-color="#211B2B"/>
<stop offset="1" stop-color="#120F18"/>
</linearGradient>
<!-- Waveform pink gradient -->
<linearGradient id="wave" x1="0" y1="0" x2="1" y2="1">
<stop offset="0%" stop-color="#e8a4b8"/>
<stop offset="50%" stop-color="#d3869b"/>
<stop offset="100%" stop-color="#c07090"/>
</linearGradient>
<!-- Glow filter -->
<filter id="glow" x="-50%" y="-50%" width="200%" height="200%">
<feGaussianBlur in="SourceGraphic" stdDeviation="8" result="blur"/>
<feColorMatrix in="blur" type="matrix" values="1 0 0 0 0 0 0.4 0 0 0 0 0 0.5 0 0 0 0 0 0.6 0" result="glow"/>
<feMerge>
<feMergeNode in="glow"/>
<feMergeNode in="SourceGraphic"/>
</feMerge>
</filter>
<!-- Subtle inner shadow -->
<filter id="inset" x="-10%" y="-10%" width="120%" height="120%">
<feGaussianBlur in="SourceAlpha" stdDeviation="6" result="blur"/>
<feOffset dx="0" dy="3" result="offset"/>
<feComposite in="SourceGraphic" in2="offset" operator="over"/>
</filter>
<!-- Edge highlight -->
<linearGradient id="edge" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#ffffff" stop-opacity="0.12"/>
<stop offset="50%" stop-color="#ffffff" stop-opacity="0.03"/>
<stop offset="100%" stop-color="#000000" stop-opacity="0.15"/>
<radialGradient id="warmth" cx="0" cy="0" r="1" gradientTransform="translate(49 13) rotate(132) scale(42)" gradientUnits="userSpaceOnUse">
<stop stop-color="#6A2D4C" stop-opacity="0.44"/>
<stop offset="1" stop-color="#6A2D4C" stop-opacity="0"/>
</radialGradient>
<linearGradient id="voice" x1="7" y1="18" x2="59" y2="46" gradientUnits="userSpaceOnUse">
<stop stop-color="#FFD0DB"/>
<stop offset="0.5" stop-color="#F47FA3"/>
<stop offset="1" stop-color="#F7AE73"/>
</linearGradient>
</defs>
<!-- Outer rounded square -->
<rect x="16" y="16" width="480" height="480" rx="96" ry="96" fill="url(#bg)"/>
<!-- Edge/border highlight -->
<rect x="16" y="16" width="480" height="480" rx="96" ry="96" fill="none" stroke="url(#edge)" stroke-width="2"/>
<!-- Central waveform bars (audio visualizer style) -->
<g transform="translate(256, 256)" filter="url(#glow)">
<!-- 7 bars, symmetric heights, rounded caps -->
<rect x="-120" y="-30" width="20" height="60" rx="10" fill="url(#wave)" opacity="0.7"/>
<rect x="-84" y="-55" width="20" height="110" rx="10" fill="url(#wave)" opacity="0.85"/>
<rect x="-48" y="-80" width="20" height="160" rx="10" fill="url(#wave)" opacity="0.95"/>
<rect x="-10" y="-100" width="20" height="200" rx="10" fill="url(#wave)"/>
<rect x="28" y="-75" width="20" height="150" rx="10" fill="url(#wave)" opacity="0.95"/>
<rect x="64" y="-50" width="20" height="100" rx="10" fill="url(#wave)" opacity="0.85"/>
<rect x="100" y="-25" width="20" height="50" rx="10" fill="url(#wave)" opacity="0.7"/>
</g>
<!-- Subtle circle ring behind bars -->
<circle cx="256" cy="256" r="140" fill="none" stroke="#d3869b" stroke-width="1.5" opacity="0.15"/>
<rect x="2" y="2" width="60" height="60" rx="15" fill="url(#tile)"/>
<rect x="2.5" y="2.5" width="59" height="59" rx="14.5" fill="url(#warmth)" stroke="#FFFFFF" stroke-opacity="0.09"/>
<path d="M6 34c4 0 5-7 9-7 5 0 4 14 9 14 5 0 4-23 9-23s4 28 9 28 4-21 9-21c4 0 5 9 8 9" stroke="url(#voice)" stroke-width="5.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M50 9c.7 4.3 3.7 7.3 8 8-4.3.7-7.3 3.7-8 8-.7-4.3-3.7-7.3-8-8 4.3-.7 7.3-3.7 8-8Z" fill="#FFE5B5"/>
</svg>

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 426 KiB

After

Width:  |  Height:  |  Size: 189 KiB

+9 -9
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
leaves your machine. See docs/agentic-voice.md for the full recipe.
VoiceStudio defaults to a local OpenAI-compatible TTS/STT provider. For remote
use, point it only at a protected tailnet URL. See docs/agentic-voice.md.
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": "5.0.0",
"private": true,
"license": "AGPL-3.0-only",
"type": "module",
+16 -56
View File
@@ -1,61 +1,21 @@
<svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" viewBox="0 0 512 512">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" role="img" aria-label="VoiceStudio">
<defs>
<!-- Background gradient -->
<linearGradient id="bg" x1="0" y1="0" x2="1" y2="1">
<stop offset="0%" stop-color="#1e1e2e"/>
<stop offset="100%" stop-color="#13131f"/>
<linearGradient id="tile" x1="7" y1="4" x2="58" y2="62" gradientUnits="userSpaceOnUse">
<stop stop-color="#211B2B"/>
<stop offset="1" stop-color="#120F18"/>
</linearGradient>
<!-- Waveform pink gradient -->
<linearGradient id="wave" x1="0" y1="0" x2="1" y2="1">
<stop offset="0%" stop-color="#e8a4b8"/>
<stop offset="50%" stop-color="#d3869b"/>
<stop offset="100%" stop-color="#c07090"/>
</linearGradient>
<!-- Glow filter -->
<filter id="glow" x="-50%" y="-50%" width="200%" height="200%">
<feGaussianBlur in="SourceGraphic" stdDeviation="8" result="blur"/>
<feColorMatrix in="blur" type="matrix" values="1 0 0 0 0 0 0.4 0 0 0 0 0 0.5 0 0 0 0 0 0.6 0" result="glow"/>
<feMerge>
<feMergeNode in="glow"/>
<feMergeNode in="SourceGraphic"/>
</feMerge>
</filter>
<!-- Subtle inner shadow -->
<filter id="inset" x="-10%" y="-10%" width="120%" height="120%">
<feGaussianBlur in="SourceAlpha" stdDeviation="6" result="blur"/>
<feOffset dx="0" dy="3" result="offset"/>
<feComposite in="SourceGraphic" in2="offset" operator="over"/>
</filter>
<!-- Edge highlight -->
<linearGradient id="edge" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#ffffff" stop-opacity="0.12"/>
<stop offset="50%" stop-color="#ffffff" stop-opacity="0.03"/>
<stop offset="100%" stop-color="#000000" stop-opacity="0.15"/>
<radialGradient id="warmth" cx="0" cy="0" r="1" gradientTransform="translate(49 13) rotate(132) scale(42)" gradientUnits="userSpaceOnUse">
<stop stop-color="#6A2D4C" stop-opacity="0.44"/>
<stop offset="1" stop-color="#6A2D4C" stop-opacity="0"/>
</radialGradient>
<linearGradient id="voice" x1="7" y1="18" x2="59" y2="46" gradientUnits="userSpaceOnUse">
<stop stop-color="#FFD0DB"/>
<stop offset="0.5" stop-color="#F47FA3"/>
<stop offset="1" stop-color="#F7AE73"/>
</linearGradient>
</defs>
<!-- Outer rounded square -->
<rect x="16" y="16" width="480" height="480" rx="96" ry="96" fill="url(#bg)"/>
<!-- Edge/border highlight -->
<rect x="16" y="16" width="480" height="480" rx="96" ry="96" fill="none" stroke="url(#edge)" stroke-width="2"/>
<!-- Central waveform bars (audio visualizer style) -->
<g transform="translate(256, 256)" filter="url(#glow)">
<!-- 7 bars, symmetric heights, rounded caps -->
<rect x="-120" y="-30" width="20" height="60" rx="10" fill="url(#wave)" opacity="0.7"/>
<rect x="-84" y="-55" width="20" height="110" rx="10" fill="url(#wave)" opacity="0.85"/>
<rect x="-48" y="-80" width="20" height="160" rx="10" fill="url(#wave)" opacity="0.95"/>
<rect x="-10" y="-100" width="20" height="200" rx="10" fill="url(#wave)"/>
<rect x="28" y="-75" width="20" height="150" rx="10" fill="url(#wave)" opacity="0.95"/>
<rect x="64" y="-50" width="20" height="100" rx="10" fill="url(#wave)" opacity="0.85"/>
<rect x="100" y="-25" width="20" height="50" rx="10" fill="url(#wave)" opacity="0.7"/>
</g>
<!-- Subtle circle ring behind bars -->
<circle cx="256" cy="256" r="140" fill="none" stroke="#d3869b" stroke-width="1.5" opacity="0.15"/>
<rect x="2" y="2" width="60" height="60" rx="15" fill="url(#tile)"/>
<rect x="2.5" y="2.5" width="59" height="59" rx="14.5" fill="url(#warmth)" stroke="#FFFFFF" stroke-opacity="0.09"/>
<path d="M6 34c4 0 5-7 9-7 5 0 4 14 9 14 5 0 4-23 9-23s4 28 9 28 4-21 9-21c4 0 5 9 8 9" stroke="url(#voice)" stroke-width="5.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M50 9c.7 4.3 3.7 7.3 8 8-4.3.7-7.3 3.7-8 8-.7-4.3-3.7-7.3-8-8 4.3-.7 7.3-3.7 8-8Z" fill="#FFE5B5"/>
</svg>

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

+1 -1
View File
@@ -2941,7 +2941,7 @@ dependencies = [
[[package]]
name = "omnivoice-studio"
version = "0.4.2"
version = "5.0.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 = "5.0.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
Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 57 KiB

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 KiB

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 70 KiB

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 83 KiB

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 KiB

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.2 KiB

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.2 KiB

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.0 KiB

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 91 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 160 KiB

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 KiB

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 222 KiB

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 733 B

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.8 KiB

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 KiB

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 KiB

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 724 KiB

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.3 KiB

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 14 KiB

+8 -15
View File
@@ -23,6 +23,7 @@ import {
import { Button, Badge } from '../ui';
import NotificationPanel from './NotificationPanel';
import TitleTabs from './TitleTabs';
import VoiceStudioMark from './brand/VoiceStudioMark';
import { useAppStore } from '../store';
import { useSysinfo } from '../api/hooks';
@@ -294,22 +295,14 @@ export default function Header({
{/* Center: logo — the tabs take this room in titlebar-tabs mode. */}
{!tabsInTitlebar && (
<div className="flex items-center gap-2 justify-self-center pointer-events-none whitespace-nowrap">
<svg
viewBox="0 0 32 32"
fill="none"
aria-hidden="true"
<div
className="flex items-center gap-2 justify-self-center pointer-events-none whitespace-nowrap"
translate="no"
>
<VoiceStudioMark
data-testid="voice-studio-logo"
className="size-6 overflow-visible"
>
<path
d="M2 16c2.2 0 2.5-4 4.2-4 2.1 0 2.2 8 4.1 8 2.1 0 2.2-16 4.3-16 2.3 0 2.1 24 4.3 24 2.1 0 2.2-19 4.2-19 2.2 0 2.1 12 4 12 1.7 0 2-5 3.9-5"
stroke="var(--chrome-accent)"
strokeWidth="2.8"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
className="size-7 overflow-visible text-[var(--chrome-accent)]"
/>
<span className="text-[0.92rem] font-semibold text-[var(--chrome-fg)] tracking-[0.02em] [font-family:var(--font-sans)] not-italic">
Voice<span className="text-[var(--chrome-accent)]">Studio</span>
</span>
+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}
@@ -0,0 +1,33 @@
import React from 'react';
/**
* VoiceStudio's compact mark: a rounded speech waveform with one small spark.
* The desktop icon uses the same silhouette on a dark tile; app chrome keeps
* it transparent so it stays crisp at titlebar sizes and in every theme.
*/
export default function VoiceStudioMark({ className = '', title, ...props }) {
return (
<svg
viewBox="0 0 64 64"
fill="none"
className={className}
role={title ? 'img' : undefined}
aria-hidden={title ? undefined : true}
{...props}
>
{title ? <title>{title}</title> : null}
<path
d="M6 34c4 0 5-7 9-7 5 0 4 14 9 14 5 0 4-23 9-23s4 28 9 28 4-21 9-21c4 0 5 9 8 9"
stroke="currentColor"
strokeWidth="5.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M50 9c.7 4.3 3.7 7.3 8 8-4.3.7-7.3 3.7-8 8-.7-4.3-3.7-7.3-8-8 4.3-.7 7.3-3.7 8-8Z"
fill="currentColor"
opacity="0.72"
/>
</svg>
);
}
+10 -1
View File
@@ -19,6 +19,7 @@ import { CATEGORY_BY_ID } from './settingsCategories';
import { useAppStore } from '../../store';
import { isTauri } from './native';
import Row from './Row';
import VoiceStudioMark from '../brand/VoiceStudioMark';
/**
* Where a failing self-check can be fixed inside the app diagnose check id
@@ -81,7 +82,15 @@ export default function AboutTab({
return (
<SettingsSection icon={Info} title={t('settings.about')}>
<Row label={t('about.app')} value="VoiceStudio" />
<Row
label={t('about.app')}
value={
<span className="inline-flex items-center gap-[var(--space-2)]" translate="no">
<VoiceStudioMark className="size-5 text-[var(--chrome-accent)]" />
VoiceStudio
</span>
}
/>
<Row label={t('about.version')} value={resolveAboutVersion(appVersion, info)} mono />
<Row
label={t('about.tauri_runtime')}
@@ -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",
+8 -6
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 = "5.0.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
@@ -13,7 +13,7 @@ readme = "README.md"
# TTS model by Han Zhu remains Apache-2.0 upstream (Apache-2.0 is AGPL-compatible).
license = "AGPL-3.0-only"
requires-python = ">=3.11"
authors = [{name = "Han Zhu"}]
authors = [{name = "Debpalash"}, {name = "Han Zhu"}]
keywords = [
"tts",
"text-to-speech",
@@ -242,9 +242,11 @@ omnivoice-demo = "omnivoice.cli.demo:main"
omnivoice-dub = "omnivoice.cli.dub:main"
[project.urls]
Homepage = "https://github.com/k2-fsa/OmniVoice"
Repository = "https://github.com/k2-fsa/OmniVoice"
"Bug Tracker" = "https://github.com/k2-fsa/OmniVoice/issues"
Homepage = "https://github.com/debpalash/VoiceStudio"
Repository = "https://github.com/debpalash/VoiceStudio"
Documentation = "https://github.com/debpalash/VoiceStudio/tree/main/docs"
"Bug Tracker" = "https://github.com/debpalash/VoiceStudio/issues"
"Upstream TTS Model" = "https://github.com/k2-fsa/OmniVoice"
[tool.uv.sources]
# Install PyTorch with CUDA support on Linux/Windows (CUDA doesn't exist for Mac).
+258
View File
@@ -0,0 +1,258 @@
#!/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") {
if (windows)
return String(value || "")
.replaceAll("/", "\\")
.toLowerCase();
return resolve(String(value || ""));
}
export function belongsToCheckout(
cwd,
command,
executable,
windows = process.platform === "win32",
checkoutRoot = CHECKOUT_ROOT,
) {
const root = normalized(checkoutRoot, windows);
const prefix = `${root}${windows ? "\\" : 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 || "")
.replaceAll("/", "\\")
.toLowerCase()
: String(command || "");
let index = haystack.indexOf(root);
while (index !== -1) {
const previous = haystack[index - 1];
const next = haystack[index + root.length];
const startsArgument = previous === undefined || previous === "=" || /\s|["']/.test(previous);
const endsPath = next === undefined || next === "/" || next === "\\" || /\s|["']/.test(next);
if (startsArgument && endsPath) return true;
index = haystack.indexOf(root, index + 1);
}
return false;
}
export function isUninspectableProcessError(error) {
return ["ENOENT", "ESRCH", "EACCES", "EPERM"].includes(error?.code);
}
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 (isUninspectableProcessError(error)) return null;
throw error;
}
}
export function stopUnixProcess(pid, force, kill = process.kill) {
try {
kill(pid, force ? "SIGKILL" : "SIGTERM");
} catch (error) {
if (error?.code !== "ESRCH") 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 {
// Windows taskkill targets a reusable PID, not the inspected process
// instance. Refuse automatic termination until it can be handle-bound.
canStop: !windows,
// macOS exposes process start time to ps at one-second resolution. That is
// sufficient for a graceful stop, but not safe proof for SIGKILL escalation.
canForce: process.platform !== "darwin",
listeners: windows ? windowsListeners : unixListeners,
inspect: windows ? inspectWindows : process.platform === "darwin" ? inspectMac : inspectLinux,
stop(pid, force) {
stopUnixProcess(pid, force);
},
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 (ops.canStop === false) {
throw new Error(
`VoiceStudio process ${pid} is using a development port; stop it in Task Manager and retry`,
);
}
if (!(await inspectSameProcess(ops, pid, first.identity))) continue;
await ops.stop(pid, false, first.identity);
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;
if (ops.canForce === false) continue;
// Revalidate immediately before escalation. A recycled PID is never killed.
if (!(await inspectSameProcess(ops, pid, first.identity))) continue;
await ops.stop(pid, true, first.identity);
}
let remaining = await ops.listeners(ports);
for (let attempt = 0; attempt < 10 && remaining.length; attempt += 1) {
await ops.sleep(50);
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);
}
+156
View File
@@ -0,0 +1,156 @@
import assert from "node:assert/strict";
import test from "node:test";
import {
belongsToCheckout,
clearDevPortsWith,
isUninspectableProcessError,
parseSsListeners,
parseWindowsListeners,
stopUnixProcess,
} 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("command ownership requires a checkout path boundary", () => {
const root = "/work/VoiceStudio";
assert.equal(belongsToCheckout("", `bun ${root}/scripts/dev.mjs`, "", false, root), true);
assert.equal(belongsToCheckout("", `bun '${root}'`, "", false, root), true);
assert.equal(belongsToCheckout("", `bun --cwd=${root}/frontend`, "", false, root), true);
assert.equal(belongsToCheckout("", `bun ${root}-old/scripts/dev.mjs`, "", false, root), false);
assert.equal(belongsToCheckout("", `bun ${root}2/scripts/dev.mjs`, "", false, root), false);
assert.equal(belongsToCheckout("", `bun /tmp${root}/scripts/dev.mjs`, "", false, root), false);
assert.equal(belongsToCheckout("", "bun C:/repo/scripts/dev.mjs", "", true, "C:\\repo"), true);
});
test("permission and exit races make a process uninspectable", () => {
for (const code of ["ENOENT", "ESRCH", "EACCES", "EPERM"]) {
assert.equal(isUninspectableProcessError({ code }), true);
}
assert.equal(isUninspectableProcessError({ code: "EIO" }), false);
});
test("an already-exited Unix process counts as stopped", () => {
const missing = Object.assign(new Error("gone"), { code: "ESRCH" });
assert.doesNotThrow(() =>
stopUnixProcess(1234, false, () => {
throw missing;
}),
);
assert.throws(
() =>
stopUnixProcess(1234, true, () => {
throw Object.assign(new Error("denied"), { code: "EPERM" });
}),
/denied/,
);
});
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("refuses Windows auto-stop when termination cannot bind to the inspected process", async () => {
const signals = [];
const ops = {
canStop: false,
listeners: async () => [1234],
inspect: async () => ({ identity: "windows:start-a", owned: true }),
stop: async (...args) => signals.push(args),
sleep: async () => {},
};
await assert.rejects(clearDevPortsWith([3900], ops), /stop it in Task Manager and retry/);
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],
]);
});
test("does not force-kill when the platform cannot prove process identity precisely", async () => {
const signals = [];
let discovery = 0;
const ops = {
canForce: false,
listeners: async () => (discovery++ === 0 ? [1234] : []),
inspect: async () => ({ identity: "mac:start-second", owned: true }),
stop: async (pid, force) => signals.push([pid, force]),
sleep: async () => {},
};
await clearDevPortsWith([3900], ops);
assert.deepEqual(signals, [[1234, false]]);
});
test("waits for the listener to disappear after force stop", async () => {
const signals = [];
let discovery = 0;
let sleeps = 0;
const ops = {
listeners: async () => (++discovery < 4 ? [1234] : []),
inspect: async () => ({ identity: "start-a", owned: true }),
stop: async (pid, force) => signals.push([pid, force]),
sleep: async () => {
sleeps += 1;
},
};
await clearDevPortsWith([3900], ops);
assert.deepEqual(signals, [
[1234, false],
[1234, true],
]);
assert.equal(sleeps, 12);
});
+126
View File
@@ -0,0 +1,126 @@
"""VoiceStudio 5.0.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]
RELEASE_VERSION = "5.0.0"
def test_release_version_is_5_0_0_everywhere() -> None:
package = json.loads((ROOT / "frontend/package.json").read_text())
assert package["version"] == RELEASE_VERSION
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) == RELEASE_VERSION, path
lock_contracts = {
"bun.lock": r'"name": "omnivoice-studio",\s+"version": "([^"]+)"',
"uv.lock": r'name = "omnivoice"\s+version = "([^"]+)"',
"frontend/src-tauri/Cargo.lock": (
r'name = "omnivoice-studio"\s+version = "([^"]+)"'
),
}
for path, pattern in lock_contracts.items():
match = re.search(pattern, (ROOT / path).read_text())
assert match and match.group(1) == RELEASE_VERSION, path
def test_visible_brand_surfaces_say_voicestudio() -> None:
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_brand_mark_is_shared_and_fills_the_icon() -> None:
mark = (ROOT / "frontend/src/components/brand/VoiceStudioMark.jsx").read_text()
header = (ROOT / "frontend/src/components/Header.jsx").read_text()
about = (ROOT / "frontend/src/components/settings/AboutTab.jsx").read_text()
logo = (ROOT / "docs/logo.svg").read_text()
favicon = (ROOT / "frontend/public/favicon.svg").read_text()
signature = "M6 34c4 0 5-7 9-7"
assert signature in mark
assert signature in logo
assert signature in favicon
assert "<VoiceStudioMark" in header
assert "<VoiceStudioMark" in about
assert 'data-testid="voice-studio-logo"' in header
# The previous icon devoted most of its canvas to an empty ring. The new
# mark uses the full tile and keeps only a narrow 2-unit outer margin.
assert 'x="2" y="2" width="60" height="60"' in logo
assert "<circle" not in logo
assert 'src="docs/logo.png"' in (ROOT / "README.md").read_text()
def test_python_package_metadata_points_to_voicestudio() -> None:
pyproject = (ROOT / "pyproject.toml").read_text()
assert 'Homepage = "https://github.com/debpalash/VoiceStudio"' in pyproject
assert 'Repository = "https://github.com/debpalash/VoiceStudio"' in pyproject
assert '"Upstream TTS Model" = "https://github.com/k2-fsa/OmniVoice"' in pyproject
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_compatibility_identifiers_stay_stable() -> None:
package = json.loads((ROOT / "frontend/package.json").read_text())
assert package["name"] == "omnivoice-studio"
assert 'name = "omnivoice"' in (ROOT / "pyproject.toml").read_text()
assert 'name = "omnivoice-studio"' in (
ROOT / "frontend/src-tauri/Cargo.toml"
).read_text()
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 = "5.0.0"
source = { editable = "." }
dependencies = [
{ name = "accelerate" },