Release VoiceStudio 5.0.0 (#1487)
Complete the VoiceStudio identity, release documentation, assets, version mirrors, and safe cross-platform development startup.
@@ -202,6 +202,7 @@ class MyEngineBackend(TTSBackend):
|
|||||||
|
|
||||||
- **Components**: Functional components with hooks
|
- **Components**: Functional components with hooks
|
||||||
- **State**: Zustand stores in `src/stores/`, organized by slice
|
- **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`.)
|
- **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
|
- **Naming**: `PascalCase` for components, `camelCase` for hooks and utils
|
||||||
|
|
||||||
|
|||||||
@@ -45,6 +45,6 @@ body:
|
|||||||
- type: markdown
|
- type: markdown
|
||||||
attributes:
|
attributes:
|
||||||
value: |
|
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
|
API keys, or cloud calls, and behave identically on macOS/Windows/Linux. Proposals
|
||||||
that fit those constraints are easiest to land.
|
that fit those constraints are easiest to land.
|
||||||
|
|||||||
@@ -29,7 +29,7 @@
|
|||||||
# On main pushes the Docker Hub repository overview is also synced from
|
# 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).
|
# 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
|
# backend + pre-built React frontend served over HTTP). The Tauri desktop
|
||||||
# auto-updater and its update-channel toggle are desktop-only features; they do
|
# auto-updater and its update-channel toggle are desktop-only features; they do
|
||||||
# NOT apply to the Docker image.
|
# NOT apply to the Docker image.
|
||||||
|
|||||||
@@ -3,13 +3,19 @@
|
|||||||
All notable changes to VoiceStudio.
|
All notable changes to VoiceStudio.
|
||||||
|
|
||||||
The format is loosely based on [Keep a Changelog](https://keepachangelog.com/).
|
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`).
|
`frontend/package.json` is the app-version source of truth; Cargo, Python, and
|
||||||
The bundled TTS model package (`pyproject.toml`) is versioned independently.
|
the frozen-backend fallback mirror it for their toolchains.
|
||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
## [5.0.0] — 2026-08-11
|
||||||
|
|
||||||
**Highlights**
|
**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
|
- 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
|
- 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
|
- 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
|
### 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)
|
- 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 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.
|
- 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.
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
<div align="center">
|
<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>
|
<h1>VoiceStudio</h1>
|
||||||
<p><sub><em>previously OmniVoice-Studio</em></sub></p>
|
<p><sub><em>previously OmniVoice-Studio</em></sub></p>
|
||||||
<h3>The open-source ElevenLabs alternative.</h3>
|
<h3>Make voices. Tell stories. Keep the files. ♡</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>
|
<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>
|
<p>
|
||||||
<a href="#quickstart">Quickstart</a> ·
|
<a href="#quickstart">Quickstart</a> ·
|
||||||
<a href="#features">Features</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="#tts-engines">Engines</a> ·
|
||||||
<a href="#openai-api">API</a> ·
|
<a href="#openai-api">API</a> ·
|
||||||
<a href="#sponsor--donate">Donate</a> ·
|
<a href="#sponsor--donate">Donate</a> ·
|
||||||
@@ -48,56 +48,11 @@
|
|||||||
<img src="docs/screenshot-launchpad.png" alt="VoiceStudio — Launchpad" width="100%"/>
|
<img src="docs/screenshot-launchpad.png" alt="VoiceStudio — Launchpad" width="100%"/>
|
||||||
</div>
|
</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]
|
> [!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).
|
> **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>
|
<a id="features"></a>
|
||||||
|
|
||||||
## ✨ Features
|
## ✨ 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>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>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>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>
|
<td align="center" width="20%">🤖<br/><b>MCP Server</b><br/><sub>Use from Claude, Cursor, …</sub></td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</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** |
|
| | **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 Cloning** | ✅ 3s clip | ✅ 3s clip, zero-shot |
|
||||||
| **Voice Design** | ✅ Gender, age | ✅ Gender, age, accent, pitch, style, dialect |
|
| **Voice Design** | ✅ Gender, age | ✅ Gender, age, accent, pitch, style, dialect |
|
||||||
| **Audiobook / Stories** | ❌ | ✅ Full audiobook editor + multi-voice stories (EPUB/PDF import, .m4b export) |
|
| **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 |
|
| **Video Dubbing** | ✅ Cloud-only | ✅ Fully local |
|
||||||
| **Data Privacy** | Audio sent to cloud | **Nothing leaves your machine** |
|
| **Data Privacy** | Audio is processed remotely | Core workflow runs locally; online services are explicit opt-ins |
|
||||||
| **API Keys** | Required | Not needed |
|
| **API Keys** | Account required | Not needed for the local workflow |
|
||||||
| **GPU Support** | N/A (cloud) | CUDA · Apple Silicon · ROCm (Linux) · CPU |
|
| **GPU Support** | N/A (cloud) | CUDA · Apple Silicon · ROCm (Linux) · CPU |
|
||||||
| **Desktop App** | ❌ | ✅ macOS · Windows · Linux |
|
| **Desktop App** | ❌ | ✅ macOS · Windows · Linux |
|
||||||
| **TTS Engines** | 1 | **14** — [full matrix](#tts-engines) |
|
| **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 |
|
| 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 |
|
| **CosyVoice 3** | 9 + 18 dialects | ✅ | ✅ | ✅ CUDA/CPU | ✅ MPS | ✅ CUDA/CPU | Apache-2.0 |
|
||||||
| **GPT-SoVITS** | 5 | ✅ | — | ✅ CUDA/CPU | — | ✅ CUDA/CPU | MIT |
|
| **GPT-SoVITS** | 5 | ✅ | — | ✅ CUDA/CPU | — | ✅ CUDA/CPU | MIT |
|
||||||
| **VoxCPM2** | 30 | ✅ | ✅ | ✅ CUDA/CPU | ✅ MPS | ✅ CUDA/CPU | Apache-2.0 |
|
| **VoxCPM2** | 30 | ✅ | ✅ | ✅ CUDA/CPU | ✅ MPS | ✅ CUDA/CPU | Apache-2.0 |
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
*本文档是 [README.md](README.md) 的简体中文翻译;若与英文版有出入,以英文版为准。*
|
*本文档是 [README.md](README.md) 的简体中文翻译;若与英文版有出入,以英文版为准。*
|
||||||
|
|
||||||
<div align="center">
|
<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>
|
<h1>VoiceStudio</h1>
|
||||||
<p><sub><em>原名 OmniVoice-Studio</em></sub></p>
|
<p><sub><em>原名 OmniVoice-Studio</em></sub></p>
|
||||||
<h3>开源版 ElevenLabs 替代品。</h3>
|
<h3>创造声音,讲述故事,文件始终属于你。♡</h3>
|
||||||
<p>实时听写、零样本语音克隆、电影级视频配音——全部在你的桌面上完成。<br/><b>无需账号。无需 API 密钥。无需云端。</b>一切都在你自己的设备上运行。开源,支持 <b>646 种语言</b>。</p>
|
<p>在一个开源桌面工作室里完成克隆、设计、配音、听写和有声书制作。<br/><b>默认本地优先。</b>没有订阅,也没有用量计费;联网服务始终由你主动选择。</p>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
<a href="#quickstart">快速开始</a> ·
|
<a href="#quickstart">快速开始</a> ·
|
||||||
<a href="#features">功能</a> ·
|
<a href="#features">功能</a> ·
|
||||||
<a href="#why-ovs">为什么选择 OVS</a> ·
|
<a href="#why-voicestudio">为什么选择 VoiceStudio</a> ·
|
||||||
<a href="#tts-engines">引擎</a> ·
|
<a href="#tts-engines">引擎</a> ·
|
||||||
<a href="#openai-api">API</a> ·
|
<a href="#openai-api">API</a> ·
|
||||||
<a href="#sponsor--donate">捐赠</a> ·
|
<a href="#sponsor--donate">捐赠</a> ·
|
||||||
@@ -40,56 +40,11 @@
|
|||||||
<img src="docs/screenshot-launchpad.png" alt="VoiceStudio — 启动台" width="100%"/>
|
<img src="docs/screenshot-launchpad.png" alt="VoiceStudio — 启动台" width="100%"/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
> **你的声音是你最私密的数据。为什么还要按月付费,从云端把它租回来?** 每一款主流语音工具都会把你的音频送到别人的服务器上,并按月向你收费。VoiceStudio 反其道而行:克隆、设计、配音、听写,全部在你自己的硬件上完成——646 种语言,没有计费表在转,任何数据都不离开你的设备。
|
> **声音很私人,创作空间也应该真正属于你。** VoiceStudio 的核心流程运行在你的硬件上:克隆、设计、配音、听写,并以 646 种语言创作,不需要订阅,也没有用量计费。联网引擎和服务始终是清晰可见的可选项,而不是隐藏依赖。
|
||||||
|
|
||||||
> [!WARNING]
|
> [!WARNING]
|
||||||
> **活跃 Beta 阶段。** 各版本之间可能出现故障——如需最新修复,请从源码运行。非常欢迎 Bug 报告和 PR:[提交 Issue](https://github.com/debpalash/VoiceStudio/issues) 或 [加入 Discord](https://discord.gg/bzQavDfVV9)。
|
> **活跃 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>
|
<a id="features"></a>
|
||||||
|
|
||||||
## ✨ 功能
|
## ✨ 功能
|
||||||
@@ -125,8 +80,8 @@
|
|||||||
<p>在<b>任何应用</b>中按 <kbd>⌘</kbd>+<kbd>⇧</kbd>+<kbd>Space</kbd>。<br/>转录、自动粘贴、随即消失。</p>
|
<p>在<b>任何应用</b>中按 <kbd>⌘</kbd>+<kbd>⇧</kbd>+<kbd>Space</kbd>。<br/>转录、自动粘贴、随即消失。</p>
|
||||||
</td>
|
</td>
|
||||||
<td align="center" valign="top">
|
<td align="center" valign="top">
|
||||||
<h3>🔐 100% 本地</h3>
|
<h3>🔐 本地优先</h3>
|
||||||
<p>无需密钥、无需云端、无需账号。<br/><b>只在你的设备上</b>。</p>
|
<p>核心创作流程<br/><b>留在你的设备上</b>。</p>
|
||||||
</td>
|
</td>
|
||||||
<td align="center" valign="top">
|
<td align="center" valign="top">
|
||||||
<h3>🤖 MCP 服务器</h3>
|
<h3>🤖 MCP 服务器</h3>
|
||||||
@@ -200,22 +155,22 @@ Hugging Face Token 的配置见
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
<a id="why-ovs"></a>
|
<a id="why-voicestudio"></a>
|
||||||
|
|
||||||
## 💡 为什么选择 VoiceStudio?
|
## 💡 为什么选择 VoiceStudio?
|
||||||
|
|
||||||
ElevenLabs 收费 **$5–$330/月**,并在他们的服务器上处理你的音频。VoiceStudio **在你的硬件上运行,没有任何用量限制。**
|
云端语音工具很方便,但工作流会依赖账号、用量计费和他人的基础设施。VoiceStudio 在你的硬件上提供完整工作室;只有你主动选择时,才会使用联网集成。
|
||||||
|
|
||||||
| | **ElevenLabs** | **VoiceStudio** |
|
| | **ElevenLabs** | **VoiceStudio** |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| **价格** | $5–$330/月,按字符计费 | 免费且开源(AGPL-3.0)· 专有用途可选 [商业许可证](#license) |
|
| **价格** | 订阅与用量限制 | 免费且开源(AGPL-3.0)· 专有用途可选 [商业许可证](#license) |
|
||||||
| **语音克隆** | ✅ 3 秒音频 | ✅ 3 秒音频,零样本 |
|
| **语音克隆** | ✅ 3 秒音频 | ✅ 3 秒音频,零样本 |
|
||||||
| **声音设计** | ✅ 性别、年龄 | ✅ 性别、年龄、口音、音高、风格、方言 |
|
| **声音设计** | ✅ 性别、年龄 | ✅ 性别、年龄、口音、音高、风格、方言 |
|
||||||
| **有声书 / 故事** | ❌ | ✅ 完整有声书编辑器 + 多声音故事(EPUB/PDF 导入,.m4b 导出) |
|
| **有声书 / 故事** | ❌ | ✅ 完整有声书编辑器 + 多声音故事(EPUB/PDF 导入,.m4b 导出) |
|
||||||
| **语言** | 32 | **646** |
|
| **语言** | 取决于套餐和模型 | **646** |
|
||||||
| **视频配音** | ✅ 仅云端 | ✅ 完全本地 |
|
| **视频配音** | ✅ 仅云端 | ✅ 完全本地 |
|
||||||
| **数据隐私** | 音频发送到云端 | **数据不离开你的设备** |
|
| **数据隐私** | 音频在远端处理 | 核心流程在本地运行;联网服务必须主动选择 |
|
||||||
| **API 密钥** | 需要 | 不需要 |
|
| **API 密钥** | 需要账号 | 本地流程不需要 |
|
||||||
| **GPU 支持** | 不适用(云端) | CUDA · Apple Silicon · ROCm(Linux)· CPU |
|
| **GPU 支持** | 不适用(云端) | CUDA · Apple Silicon · ROCm(Linux)· CPU |
|
||||||
| **桌面应用** | ❌ | ✅ macOS · Windows · Linux |
|
| **桌面应用** | ❌ | ✅ macOS · Windows · Linux |
|
||||||
| **TTS 引擎** | 1 | **14** — [完整矩阵](#tts-engines) |
|
| **TTS 引擎** | 1 | **14** — [完整矩阵](#tts-engines) |
|
||||||
@@ -268,7 +223,7 @@ ElevenLabs 收费 **$5–$330/月**,并在他们的服务器上处理你的音
|
|||||||
|
|
||||||
| 引擎 | 语言 | 克隆 | 指令 | Linux | macOS ARM | Windows | 许可证 |
|
| 引擎 | 语言 | 克隆 | 指令 | 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 |
|
| **CosyVoice 3** | 9 + 18 种方言 | ✅ | ✅ | ✅ CUDA/CPU | ✅ MPS | ✅ CUDA/CPU | Apache-2.0 |
|
||||||
| **GPT-SoVITS** | 5 | ✅ | — | ✅ CUDA/CPU | — | ✅ CUDA/CPU | MIT |
|
| **GPT-SoVITS** | 5 | ✅ | — | ✅ CUDA/CPU | — | ✅ CUDA/CPU | MIT |
|
||||||
| **VoxCPM2** | 30 | ✅ | ✅ | ✅ CUDA/CPU | ✅ MPS | ✅ CUDA/CPU | Apache-2.0 |
|
| **VoxCPM2** | 30 | ✅ | ✅ | ✅ CUDA/CPU | ✅ MPS | ✅ CUDA/CPU | Apache-2.0 |
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<div align="center">
|
<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>
|
<h1>Sponsor VoiceStudio</h1>
|
||||||
<p><b>Keep the open-source ElevenLabs alternative free, local, and shipping.</b></p>
|
<p><b>Keep the open-source ElevenLabs alternative free, local, and shipping.</b></p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ models:
|
|||||||
# ── Required ──────────────────────────────────────────────────────────
|
# ── Required ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
- repo_id: "k2-fsa/OmniVoice"
|
- repo_id: "k2-fsa/OmniVoice"
|
||||||
label: "OmniVoice TTS (600+ languages, zero-shot)"
|
label: "VoiceStudio TTS (k2-fsa/OmniVoice, 600+ languages, zero-shot)"
|
||||||
role: TTS
|
role: TTS
|
||||||
size_gb: 2.4
|
size_gb: 2.4
|
||||||
required: true
|
required: true
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ from pathlib import Path
|
|||||||
# tests/test_app_version.py::test_all_version_files_in_lockstep and bumped by
|
# 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
|
# release.yml's version-bump job, so it stays equal to
|
||||||
# pyproject/tauri.conf/Cargo/package.json.
|
# pyproject/tauri.conf/Cargo/package.json.
|
||||||
_FALLBACK_VERSION = "0.4.2"
|
_FALLBACK_VERSION = "5.0.0"
|
||||||
|
|
||||||
|
|
||||||
def _fallback_version() -> str:
|
def _fallback_version() -> str:
|
||||||
|
|||||||
@@ -79,13 +79,13 @@ class Confucius4Backend(SubprocessBackend):
|
|||||||
return False, (
|
return False, (
|
||||||
"Confucius4-TTS venv not found. Set OMNIVOICE_CONFUCIUS4_TTS_DIR "
|
"Confucius4-TTS venv not found. Set OMNIVOICE_CONFUCIUS4_TTS_DIR "
|
||||||
"to your Confucius4-TTS clone (the directory containing "
|
"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."
|
"(CPU works but is slow). See docs/engines/confucius4-tts.md."
|
||||||
)
|
)
|
||||||
if not CONFUCIUS4_SIDECAR_SCRIPT.exists():
|
if not CONFUCIUS4_SIDECAR_SCRIPT.exists():
|
||||||
return False, (
|
return False, (
|
||||||
"Confucius4-TTS sidecar script missing at "
|
"Confucius4-TTS sidecar script missing at "
|
||||||
f"{CONFUCIUS4_SIDECAR_SCRIPT} — reinstall OmniVoice."
|
f"{CONFUCIUS4_SIDECAR_SCRIPT} — reinstall VoiceStudio."
|
||||||
)
|
)
|
||||||
return True, "ok"
|
return True, "ok"
|
||||||
|
|
||||||
|
|||||||
@@ -128,7 +128,7 @@ def resolve_confucius4_venv() -> Path:
|
|||||||
"Confucius4-TTS is not installed. Set the "
|
"Confucius4-TTS is not installed. Set the "
|
||||||
f"{_CLONE_DIR_ENV} environment variable to your Confucius4-TTS clone "
|
f"{_CLONE_DIR_ENV} environment variable to your Confucius4-TTS clone "
|
||||||
"(the directory that contains requirements.txt), then restart "
|
"(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))
|
cand = _bootstrap_engines_venv(Path(clone_dir))
|
||||||
@@ -186,7 +186,7 @@ def _bootstrap_engines_venv(clone_dir: Path) -> Path:
|
|||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
"uv is required to bootstrap the Confucius4-TTS venv but was not "
|
"uv is required to bootstrap the Confucius4-TTS venv but was not "
|
||||||
"found on PATH (and OMNIVOICE_BUNDLED_UV was not set). Install uv "
|
"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(
|
logger.info(
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ class DotsTTSBackend(SubprocessBackend):
|
|||||||
if sys.platform == "win32":
|
if sys.platform == "win32":
|
||||||
return False, (
|
return False, (
|
||||||
"dots.tts is not supported on Windows — upstream targets "
|
"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."
|
"Linux/macOS host. See docs/engines/dots-tts.md."
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -101,13 +101,13 @@ class DotsTTSBackend(SubprocessBackend):
|
|||||||
return False, (
|
return False, (
|
||||||
"dots.tts venv not found. Set OMNIVOICE_DOTS_TTS_DIR to your "
|
"dots.tts venv not found. Set OMNIVOICE_DOTS_TTS_DIR to your "
|
||||||
"dots.tts clone (the directory containing pyproject.toml) and "
|
"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."
|
"docs/engines/dots-tts.md for the full install walk-through."
|
||||||
)
|
)
|
||||||
if not DOTS_TTS_SIDECAR_SCRIPT.exists():
|
if not DOTS_TTS_SIDECAR_SCRIPT.exists():
|
||||||
return False, (
|
return False, (
|
||||||
"dots.tts sidecar script missing at "
|
"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)"
|
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 "
|
"dots.tts is not installed. Set the "
|
||||||
f"{_CLONE_DIR_ENV} environment variable to your dots.tts clone "
|
f"{_CLONE_DIR_ENV} environment variable to your dots.tts clone "
|
||||||
"(the directory that contains pyproject.toml and constraints/), "
|
"(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."
|
"full install walk-through."
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -192,7 +192,7 @@ def _bootstrap_engines_venv(clone_dir: Path) -> Path:
|
|||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
"uv is required to bootstrap the dots.tts venv but was not found "
|
"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 "
|
"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."
|
"OMNIVOICE_BUNDLED_UV to the absolute path of a uv binary."
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -111,13 +111,13 @@ class MossTTSV15Backend(SubprocessBackend):
|
|||||||
return False, (
|
return False, (
|
||||||
"MOSS-TTS-v1.5 venv not found. Set OMNIVOICE_MOSS_TTS_V15_DIR "
|
"MOSS-TTS-v1.5 venv not found. Set OMNIVOICE_MOSS_TTS_V15_DIR "
|
||||||
"to your MOSS-TTS clone (the directory containing pyproject.toml) "
|
"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."
|
"docs/engines/moss-tts-v15.md for the full install walk-through."
|
||||||
)
|
)
|
||||||
if not MOSS_TTS_V15_SIDECAR_SCRIPT.exists():
|
if not MOSS_TTS_V15_SIDECAR_SCRIPT.exists():
|
||||||
return False, (
|
return False, (
|
||||||
"MOSS-TTS-v1.5 sidecar script missing at "
|
"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)"
|
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 "
|
"MOSS-TTS-v1.5 is not installed. Set the "
|
||||||
f"{_CLONE_DIR_ENV} environment variable to your MOSS-TTS clone "
|
f"{_CLONE_DIR_ENV} environment variable to your MOSS-TTS clone "
|
||||||
"(the directory that contains pyproject.toml), then restart "
|
"(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."
|
"install walk-through."
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -231,7 +231,7 @@ def _bootstrap_engines_venv(clone_dir: Path) -> Path:
|
|||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
"uv is required to bootstrap the MOSS-TTS-v1.5 venv but was not "
|
"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 "
|
"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."
|
"OMNIVOICE_BUNDLED_UV to the absolute path of a uv binary."
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -513,7 +513,7 @@ class OmniVoiceBackend(TTSBackend):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
id = "omnivoice"
|
id = "omnivoice"
|
||||||
display_name = "OmniVoice (600 languages, zero-shot)"
|
display_name = "VoiceStudio (k2-fsa/OmniVoice, 600+ languages)"
|
||||||
gpu_compat = ("cuda", "mps", "cpu")
|
gpu_compat = ("cuda", "mps", "cpu")
|
||||||
# Derived from the pool's own per-job budget (_GPU_VRAM_PER_JOB_GB = 5.0 in
|
# 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
|
# model_manager, itself measured from the ~1.6 GB forward + autoregressive
|
||||||
@@ -1785,7 +1785,7 @@ class SherpaOnnxBackend(TTSBackend):
|
|||||||
• Android / iOS
|
• Android / iOS
|
||||||
• WebAssembly (browser)
|
• 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.
|
on desktop and compiles to WASM for the web UI.
|
||||||
|
|
||||||
Install: pip install sherpa-onnx
|
Install: pip install sherpa-onnx
|
||||||
@@ -1826,7 +1826,7 @@ class SherpaOnnxBackend(TTSBackend):
|
|||||||
return False, (
|
return False, (
|
||||||
"OMNIVOICE_SHERPA_MODEL not set. Point it to a sherpa-onnx TTS "
|
"OMNIVOICE_SHERPA_MODEL not set. Point it to a sherpa-onnx TTS "
|
||||||
"model directory (containing model.onnx + tokens.txt), then "
|
"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"
|
"https://github.com/k2-fsa/sherpa-onnx/releases"
|
||||||
)
|
)
|
||||||
if not os.path.isfile(os.path.join(model_dir, "model.onnx")):
|
if not os.path.isfile(os.path.join(model_dir, "model.onnx")):
|
||||||
|
|||||||
@@ -6,7 +6,6 @@
|
|||||||
"name": "omnivoice-studio-monorepo",
|
"name": "omnivoice-studio-monorepo",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"concurrently": "^9.2.1",
|
"concurrently": "^9.2.1",
|
||||||
"kill-port-process": "^4.0.2",
|
|
||||||
"playwright": "^1.60.0",
|
"playwright": "^1.60.0",
|
||||||
"taze": "^19.14.1",
|
"taze": "^19.14.1",
|
||||||
"turbo": "^2.9.18",
|
"turbo": "^2.9.18",
|
||||||
@@ -16,7 +15,7 @@
|
|||||||
},
|
},
|
||||||
"frontend": {
|
"frontend": {
|
||||||
"name": "omnivoice-studio",
|
"name": "omnivoice-studio",
|
||||||
"version": "0.3.21",
|
"version": "5.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fontsource-variable/inter": "^5.2.8",
|
"@fontsource-variable/inter": "^5.2.8",
|
||||||
"@fontsource-variable/source-serif-4": "^5.2.9",
|
"@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=="],
|
"@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=="],
|
"@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=="],
|
"@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=="],
|
"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=="],
|
"expect-type": ["expect-type@1.3.0", "", {}, "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA=="],
|
||||||
|
|
||||||
"extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="],
|
"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=="],
|
"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=="],
|
"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=="],
|
"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-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=="],
|
"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=="],
|
"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=="],
|
"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": ["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=="],
|
"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-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=="],
|
"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=="],
|
"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=="],
|
"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=="],
|
"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=="],
|
"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=="],
|
"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=="],
|
"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=="],
|
"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=="],
|
"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": ["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=="],
|
"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-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-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=="],
|
"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=="],
|
"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=="],
|
"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=="],
|
"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=="],
|
"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": ["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=="],
|
"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=="],
|
"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=="],
|
"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=="],
|
"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.
|
# Open http://localhost:3900 once the health check passes.
|
||||||
#
|
#
|
||||||
# SECURITY: The port is bound to 127.0.0.1 by default — only this
|
# 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
|
# 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
|
# authentication — if you expose it, put it behind a reverse proxy
|
||||||
# with auth (Caddy basic_auth, nginx + htpasswd, Tailscale, etc.).
|
# with auth (Caddy basic_auth, nginx + htpasswd, Tailscale, etc.).
|
||||||
# ──────────────────────────────────────────────────────────────
|
# ──────────────────────────────────────────────────────────────
|
||||||
|
|||||||
@@ -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 16–512 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.
|
||||||
@@ -18,7 +18,7 @@ features:
|
|||||||
- Batch Queue
|
- Batch Queue
|
||||||
- MCP Server
|
- MCP Server
|
||||||
- AI Watermark
|
- AI Watermark
|
||||||
- 100% Local
|
- Local-first
|
||||||
- GPU Auto-Detect
|
- GPU Auto-Detect
|
||||||
- Extensible
|
- Extensible
|
||||||
|
|
||||||
@@ -28,7 +28,7 @@ features:
|
|||||||
# doc (optional): a repo-relative doc file that must exist.
|
# doc (optional): a repo-relative doc file that must exist.
|
||||||
tts_engines:
|
tts_engines:
|
||||||
- id: omnivoice
|
- id: omnivoice
|
||||||
readme: "**OmniVoice** (default)"
|
readme: "**VoiceStudio** (default, powered by k2-fsa/OmniVoice)"
|
||||||
- id: omnivoice-subprocess
|
- id: omnivoice-subprocess
|
||||||
doc: docs/engines/omnivoice-subprocess.md
|
doc: docs/engines/omnivoice-subprocess.md
|
||||||
- id: cosyvoice
|
- id: cosyvoice
|
||||||
@@ -85,6 +85,7 @@ asr_engines:
|
|||||||
|
|
||||||
# Doc files that must exist (the install path users are sent to).
|
# Doc files that must exist (the install path users are sent to).
|
||||||
docs:
|
docs:
|
||||||
|
- docs/branding.md
|
||||||
- docs/install/macos.md
|
- docs/install/macos.md
|
||||||
- docs/install/windows.md
|
- docs/install/windows.md
|
||||||
- docs/install/linux.md
|
- docs/install/linux.md
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 144 KiB After Width: | Height: | Size: 49 KiB |
@@ -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>
|
<defs>
|
||||||
<!-- Background gradient -->
|
<linearGradient id="tile" x1="7" y1="4" x2="58" y2="62" gradientUnits="userSpaceOnUse">
|
||||||
<linearGradient id="bg" x1="0" y1="0" x2="1" y2="1">
|
<stop stop-color="#211B2B"/>
|
||||||
<stop offset="0%" stop-color="#1e1e2e"/>
|
<stop offset="1" stop-color="#120F18"/>
|
||||||
<stop offset="100%" stop-color="#13131f"/>
|
|
||||||
</linearGradient>
|
</linearGradient>
|
||||||
|
<radialGradient id="warmth" cx="0" cy="0" r="1" gradientTransform="translate(49 13) rotate(132) scale(42)" gradientUnits="userSpaceOnUse">
|
||||||
<!-- Waveform pink gradient -->
|
<stop stop-color="#6A2D4C" stop-opacity="0.44"/>
|
||||||
<linearGradient id="wave" x1="0" y1="0" x2="1" y2="1">
|
<stop offset="1" stop-color="#6A2D4C" stop-opacity="0"/>
|
||||||
<stop offset="0%" stop-color="#e8a4b8"/>
|
</radialGradient>
|
||||||
<stop offset="50%" stop-color="#d3869b"/>
|
<linearGradient id="voice" x1="7" y1="18" x2="59" y2="46" gradientUnits="userSpaceOnUse">
|
||||||
<stop offset="100%" stop-color="#c07090"/>
|
<stop stop-color="#FFD0DB"/>
|
||||||
</linearGradient>
|
<stop offset="0.5" stop-color="#F47FA3"/>
|
||||||
|
<stop offset="1" stop-color="#F7AE73"/>
|
||||||
<!-- 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"/>
|
|
||||||
</linearGradient>
|
</linearGradient>
|
||||||
</defs>
|
</defs>
|
||||||
|
<rect x="2" y="2" width="60" height="60" rx="15" fill="url(#tile)"/>
|
||||||
<!-- Outer rounded square -->
|
<rect x="2.5" y="2.5" width="59" height="59" rx="14.5" fill="url(#warmth)" stroke="#FFFFFF" stroke-opacity="0.09"/>
|
||||||
<rect x="16" y="16" width="480" height="480" rx="96" ry="96" fill="url(#bg)"/>
|
<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"/>
|
||||||
<!-- 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"/>
|
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 426 KiB After Width: | Height: | Size: 189 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 defaults to a local OpenAI-compatible TTS/STT provider. For remote
|
||||||
leaves your machine. See docs/agentic-voice.md for the full recipe.
|
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]"
|
uv pip install "pipecat-ai[openai,silero]"
|
||||||
python examples/agentic/pipecat_minimal.py
|
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
|
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
|
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
|
from __future__ import annotations
|
||||||
@@ -19,7 +19,7 @@ from __future__ import annotations
|
|||||||
import os
|
import os
|
||||||
|
|
||||||
OMNIVOICE_BASE_URL = os.environ.get("OMNIVOICE_API_URL", "http://localhost:3900") + "/v1"
|
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.
|
# remote backend, pass that same value here.
|
||||||
OMNIVOICE_API_KEY = os.environ.get("OMNIVOICE_API_KEY", "not-needed-locally")
|
OMNIVOICE_API_KEY = os.environ.get("OMNIVOICE_API_KEY", "not-needed-locally")
|
||||||
# A voice-profile id from GET /v1/audio/voices, or "default".
|
# 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():
|
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
|
Imported lazily so this file is importable (and lint-clean) without
|
||||||
pipecat installed — the smoke test in CI checks the wiring shape, not a
|
pipecat installed — the smoke test in CI checks the wiring shape, not a
|
||||||
@@ -52,7 +52,7 @@ def build_services():
|
|||||||
|
|
||||||
def main() -> None:
|
def main() -> None:
|
||||||
stt, tts = build_services()
|
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("Wire `stt` and `tts` into your pipecat Pipeline with a transport")
|
||||||
print("and an LLM service. See docs/agentic-voice.md.")
|
print("and an LLM service. See docs/agentic-voice.md.")
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { test, expect } from '@playwright/test';
|
import { test, expect } from '@playwright/test';
|
||||||
import { collectErrors, gotoMode } from './_helpers';
|
import { collectErrors, gotoMode } from './_helpers';
|
||||||
|
|
||||||
test.describe('OmniVoice Gallery', () => {
|
test.describe('VoiceStudio Gallery', () => {
|
||||||
test('heading is "OmniVoice Gallery"', async ({ page }) => {
|
test('heading is "VoiceStudio Gallery"', async ({ page }) => {
|
||||||
await gotoMode(page, 'gallery');
|
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 }) => {
|
test('facet dropdowns use the dark theme, not the OS-default light surface', async ({ page }) => {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "omnivoice-studio",
|
"name": "omnivoice-studio",
|
||||||
"version": "0.4.2",
|
"version": "5.0.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"license": "AGPL-3.0-only",
|
"license": "AGPL-3.0-only",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
@@ -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>
|
<defs>
|
||||||
<!-- Background gradient -->
|
<linearGradient id="tile" x1="7" y1="4" x2="58" y2="62" gradientUnits="userSpaceOnUse">
|
||||||
<linearGradient id="bg" x1="0" y1="0" x2="1" y2="1">
|
<stop stop-color="#211B2B"/>
|
||||||
<stop offset="0%" stop-color="#1e1e2e"/>
|
<stop offset="1" stop-color="#120F18"/>
|
||||||
<stop offset="100%" stop-color="#13131f"/>
|
|
||||||
</linearGradient>
|
</linearGradient>
|
||||||
|
<radialGradient id="warmth" cx="0" cy="0" r="1" gradientTransform="translate(49 13) rotate(132) scale(42)" gradientUnits="userSpaceOnUse">
|
||||||
<!-- Waveform pink gradient -->
|
<stop stop-color="#6A2D4C" stop-opacity="0.44"/>
|
||||||
<linearGradient id="wave" x1="0" y1="0" x2="1" y2="1">
|
<stop offset="1" stop-color="#6A2D4C" stop-opacity="0"/>
|
||||||
<stop offset="0%" stop-color="#e8a4b8"/>
|
</radialGradient>
|
||||||
<stop offset="50%" stop-color="#d3869b"/>
|
<linearGradient id="voice" x1="7" y1="18" x2="59" y2="46" gradientUnits="userSpaceOnUse">
|
||||||
<stop offset="100%" stop-color="#c07090"/>
|
<stop stop-color="#FFD0DB"/>
|
||||||
</linearGradient>
|
<stop offset="0.5" stop-color="#F47FA3"/>
|
||||||
|
<stop offset="1" stop-color="#F7AE73"/>
|
||||||
<!-- 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"/>
|
|
||||||
</linearGradient>
|
</linearGradient>
|
||||||
</defs>
|
</defs>
|
||||||
|
<rect x="2" y="2" width="60" height="60" rx="15" fill="url(#tile)"/>
|
||||||
<!-- Outer rounded square -->
|
<rect x="2.5" y="2.5" width="59" height="59" rx="14.5" fill="url(#warmth)" stroke="#FFFFFF" stroke-opacity="0.09"/>
|
||||||
<rect x="16" y="16" width="480" height="480" rx="96" ry="96" fill="url(#bg)"/>
|
<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"/>
|
||||||
<!-- 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"/>
|
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 1.3 KiB |
@@ -2941,7 +2941,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "omnivoice-studio"
|
name = "omnivoice-studio"
|
||||||
version = "0.4.2"
|
version = "5.0.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"arboard",
|
"arboard",
|
||||||
"dirs-next",
|
"dirs-next",
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
# launcher's pkill matches `omnivoice-studio` and must never match a user's
|
# launcher's pkill matches `omnivoice-studio` and must never match a user's
|
||||||
# installed app. Renaming it would collapse that distinction.
|
# installed app. Renaming it would collapse that distinction.
|
||||||
name = "omnivoice-studio"
|
name = "omnivoice-studio"
|
||||||
version = "0.4.2"
|
version = "5.0.0"
|
||||||
description = "VoiceStudio – AI voice cloning & dubbing desktop app"
|
description = "VoiceStudio – AI voice cloning & dubbing desktop app"
|
||||||
authors = ["Debpalash"]
|
authors = ["Debpalash"]
|
||||||
license = "AGPL-3.0-only"
|
license = "AGPL-3.0-only"
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
into the app's Info.plist by tauri-bundler at bundle time.
|
into the app's Info.plist by tauri-bundler at bundle time.
|
||||||
-->
|
-->
|
||||||
<key>NSMicrophoneUsageDescription</key>
|
<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
|
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.
|
need this string. Cheap to ship now; avoids a future TCC denial.
|
||||||
-->
|
-->
|
||||||
<key>NSCameraUsageDescription</key>
|
<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>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|||||||
@@ -301,7 +301,7 @@ if _prefer_system_gstreamer; then
|
|||||||
# Fail safe, and say so: the app still starts on the bundled core, which
|
# 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
|
# is where the microphone problem lives, so the user needs a thread to
|
||||||
# pull rather than a silent half-fix.
|
# 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 " 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 " capture may not find any microphone (see issue #1333). Building" >&2
|
||||||
echo " from source avoids the mismatch entirely." >&2
|
echo " from source avoids the mismatch entirely." >&2
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 57 KiB After Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 4.7 KiB After Width: | Height: | Size: 4.6 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 8.7 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 70 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 83 KiB After Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 5.5 KiB After Width: | Height: | Size: 5.2 KiB |
|
Before Width: | Height: | Size: 8.2 KiB After Width: | Height: | Size: 6.8 KiB |
|
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 3.7 KiB |
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 3.5 KiB |
|
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 3.6 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 8.8 KiB |
|
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 7.2 KiB After Width: | Height: | Size: 8.8 KiB |
|
Before Width: | Height: | Size: 41 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 8.0 KiB After Width: | Height: | Size: 8.4 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 91 KiB After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 160 KiB After Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 68 KiB After Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 222 KiB After Width: | Height: | Size: 49 KiB |
|
Before Width: | Height: | Size: 733 B After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 2.5 KiB |
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 2.5 KiB |
|
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 4.0 KiB After Width: | Height: | Size: 4.0 KiB |
|
Before Width: | Height: | Size: 4.0 KiB After Width: | Height: | Size: 4.0 KiB |
|
Before Width: | Height: | Size: 7.8 KiB After Width: | Height: | Size: 6.6 KiB |
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 2.5 KiB |
|
Before Width: | Height: | Size: 6.8 KiB After Width: | Height: | Size: 5.9 KiB |
|
Before Width: | Height: | Size: 6.8 KiB After Width: | Height: | Size: 5.9 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 9.9 KiB |
|
Before Width: | Height: | Size: 724 KiB After Width: | Height: | Size: 112 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 9.9 KiB |
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 6.3 KiB After Width: | Height: | Size: 5.7 KiB |
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 14 KiB |
@@ -23,6 +23,7 @@ import {
|
|||||||
import { Button, Badge } from '../ui';
|
import { Button, Badge } from '../ui';
|
||||||
import NotificationPanel from './NotificationPanel';
|
import NotificationPanel from './NotificationPanel';
|
||||||
import TitleTabs from './TitleTabs';
|
import TitleTabs from './TitleTabs';
|
||||||
|
import VoiceStudioMark from './brand/VoiceStudioMark';
|
||||||
import { useAppStore } from '../store';
|
import { useAppStore } from '../store';
|
||||||
import { useSysinfo } from '../api/hooks';
|
import { useSysinfo } from '../api/hooks';
|
||||||
|
|
||||||
@@ -294,22 +295,14 @@ export default function Header({
|
|||||||
|
|
||||||
{/* Center: logo — the tabs take this room in titlebar-tabs mode. */}
|
{/* Center: logo — the tabs take this room in titlebar-tabs mode. */}
|
||||||
{!tabsInTitlebar && (
|
{!tabsInTitlebar && (
|
||||||
<div className="flex items-center gap-2 justify-self-center pointer-events-none whitespace-nowrap">
|
<div
|
||||||
<svg
|
className="flex items-center gap-2 justify-self-center pointer-events-none whitespace-nowrap"
|
||||||
viewBox="0 0 32 32"
|
translate="no"
|
||||||
fill="none"
|
>
|
||||||
aria-hidden="true"
|
<VoiceStudioMark
|
||||||
data-testid="voice-studio-logo"
|
data-testid="voice-studio-logo"
|
||||||
className="size-6 overflow-visible"
|
className="size-7 overflow-visible text-[var(--chrome-accent)]"
|
||||||
>
|
/>
|
||||||
<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>
|
|
||||||
<span className="text-[0.92rem] font-semibold text-[var(--chrome-fg)] tracking-[0.02em] [font-family:var(--font-sans)] not-italic">
|
<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>
|
Voice<span className="text-[var(--chrome-accent)]">Studio</span>
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -61,9 +61,9 @@ export default function NavRail({ mode, setMode, side = 'left', onFlipSide }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<aside
|
<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) => (
|
{items.map((it) => (
|
||||||
<RailBtn
|
<RailBtn
|
||||||
key={it.id}
|
key={it.id}
|
||||||
@@ -74,7 +74,7 @@ export default function NavRail({ mode, setMode, side = 'left', onFlipSide }) {
|
|||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col items-center gap-[4px]">
|
<div className="flex flex-col items-center gap-[8px]">
|
||||||
{footerItems.map((it) => (
|
{footerItems.map((it) => (
|
||||||
<RailBtn
|
<RailBtn
|
||||||
key={it.id}
|
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -19,6 +19,7 @@ import { CATEGORY_BY_ID } from './settingsCategories';
|
|||||||
import { useAppStore } from '../../store';
|
import { useAppStore } from '../../store';
|
||||||
import { isTauri } from './native';
|
import { isTauri } from './native';
|
||||||
import Row from './Row';
|
import Row from './Row';
|
||||||
|
import VoiceStudioMark from '../brand/VoiceStudioMark';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Where a failing self-check can be fixed inside the app — diagnose check id
|
* Where a failing self-check can be fixed inside the app — diagnose check id
|
||||||
@@ -81,7 +82,15 @@ export default function AboutTab({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<SettingsSection icon={Info} title={t('settings.about')}>
|
<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.version')} value={resolveAboutVersion(appVersion, info)} mono />
|
||||||
<Row
|
<Row
|
||||||
label={t('about.tauri_runtime')}
|
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();
|
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', () => {
|
describe('appShellClasses — the grid only reserves what renders', () => {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>OmniVoice — Visual Regression Harness</title>
|
<title>VoiceStudio — Visual Regression Harness</title>
|
||||||
</head>
|
</head>
|
||||||
<body style="margin: 0; background: #1d2021">
|
<body style="margin: 0; background: #1d2021">
|
||||||
<!-- Isolated component-snapshot harness. NOT part of the shipped app
|
<!-- Isolated component-snapshot harness. NOT part of the shipped app
|
||||||
|
|||||||
@@ -11,9 +11,9 @@
|
|||||||
"dev:desktop": "TAURI_SKIP_BACKEND=1 bun run --cwd frontend desktop",
|
"dev:desktop": "TAURI_SKIP_BACKEND=1 bun run --cwd frontend desktop",
|
||||||
"wait:api": "wait-on -t 300000 http-get://localhost:3900/system/info",
|
"wait:api": "wait-on -t 300000 http-get://localhost:3900/system/info",
|
||||||
"wait:frontend": "wait-on -t 120000 http://localhost:5173",
|
"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\"",
|
"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": "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": "bun scripts/desktop-prod.mjs",
|
||||||
"desktop-prod:run": "bun scripts/desktop-prod.mjs --skip-build --keep-data",
|
"desktop-prod:run": "bun scripts/desktop-prod.mjs --skip-build --keep-data",
|
||||||
@@ -36,7 +36,6 @@
|
|||||||
],
|
],
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"concurrently": "^9.2.1",
|
"concurrently": "^9.2.1",
|
||||||
"kill-port-process": "^4.0.2",
|
|
||||||
"playwright": "^1.60.0",
|
"playwright": "^1.60.0",
|
||||||
"taze": "^19.14.1",
|
"taze": "^19.14.1",
|
||||||
"turbo": "^2.9.18",
|
"turbo": "^2.9.18",
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ build-backend = "hatchling.build"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "omnivoice"
|
name = "omnivoice"
|
||||||
version = "0.4.2"
|
version = "5.0.0"
|
||||||
description = "OmniVoice: Towards Omnilingual Zero-Shot Text-to-Speech with Diffusion Language Models"
|
description = "VoiceStudio — a private, local-first studio for voice cloning, speech generation, dubbing, transcription, and audiobooks"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
# Free and open-source under the GNU Affero General Public License v3 (see
|
# Free and open-source under the GNU Affero General Public License v3 (see
|
||||||
# LICENSE). A commercial license is available for proprietary/closed-source use
|
# 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).
|
# TTS model by Han Zhu remains Apache-2.0 upstream (Apache-2.0 is AGPL-compatible).
|
||||||
license = "AGPL-3.0-only"
|
license = "AGPL-3.0-only"
|
||||||
requires-python = ">=3.11"
|
requires-python = ">=3.11"
|
||||||
authors = [{name = "Han Zhu"}]
|
authors = [{name = "Debpalash"}, {name = "Han Zhu"}]
|
||||||
keywords = [
|
keywords = [
|
||||||
"tts",
|
"tts",
|
||||||
"text-to-speech",
|
"text-to-speech",
|
||||||
@@ -242,9 +242,11 @@ omnivoice-demo = "omnivoice.cli.demo:main"
|
|||||||
omnivoice-dub = "omnivoice.cli.dub:main"
|
omnivoice-dub = "omnivoice.cli.dub:main"
|
||||||
|
|
||||||
[project.urls]
|
[project.urls]
|
||||||
Homepage = "https://github.com/k2-fsa/OmniVoice"
|
Homepage = "https://github.com/debpalash/VoiceStudio"
|
||||||
Repository = "https://github.com/k2-fsa/OmniVoice"
|
Repository = "https://github.com/debpalash/VoiceStudio"
|
||||||
"Bug Tracker" = "https://github.com/k2-fsa/OmniVoice/issues"
|
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]
|
[tool.uv.sources]
|
||||||
# Install PyTorch with CUDA support on Linux/Windows (CUDA doesn't exist for Mac).
|
# Install PyTorch with CUDA support on Linux/Windows (CUDA doesn't exist for Mac).
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
});
|
||||||
@@ -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
|
||||||
@@ -3253,7 +3253,7 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "omnivoice"
|
name = "omnivoice"
|
||||||
version = "0.4.2"
|
version = "5.0.0"
|
||||||
source = { editable = "." }
|
source = { editable = "." }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "accelerate" },
|
{ name = "accelerate" },
|
||||||
|
|||||||