Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
50427edc8f | ||
|
|
5e4bcdb134 | ||
|
|
cd32df6eb3 |
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 & 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
@@ -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 · ROCm(Linux)· 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 |
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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)"
|
||||
|
||||
|
||||
@@ -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."
|
||||
)
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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."
|
||||
)
|
||||
|
||||
|
||||
@@ -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)"
|
||||
|
||||
|
||||
@@ -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."
|
||||
)
|
||||
|
||||
|
||||
@@ -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")):
|
||||
|
||||
@@ -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=="],
|
||||
|
||||
@@ -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
@@ -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 |
@@ -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.")
|
||||
|
||||
|
||||
@@ -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,6 +1,6 @@
|
||||
{
|
||||
"name": "omnivoice-studio",
|
||||
"version": "0.4.2",
|
||||
"version": "0.5.0",
|
||||
"private": true,
|
||||
"license": "AGPL-3.0-only",
|
||||
"type": "module",
|
||||
|
||||
Generated
+1
-1
@@ -2941,7 +2941,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "omnivoice-studio"
|
||||
version = "0.4.2"
|
||||
version = "0.5.0"
|
||||
dependencies = [
|
||||
"arboard",
|
||||
"dirs-next",
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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],
|
||||
]);
|
||||
});
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user