local-tts-studio 1.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- local_tts_studio-1.0.0/.gitignore +36 -0
- local_tts_studio-1.0.0/LICENSE +21 -0
- local_tts_studio-1.0.0/PKG-INFO +101 -0
- local_tts_studio-1.0.0/README.md +71 -0
- local_tts_studio-1.0.0/local_tts/__init__.py +14 -0
- local_tts_studio-1.0.0/local_tts/_web/assets/index-BZhhl2_P.css +1 -0
- local_tts_studio-1.0.0/local_tts/_web/assets/index-CwaKVVUK.js +139 -0
- local_tts_studio-1.0.0/local_tts/_web/favicon.svg +10 -0
- local_tts_studio-1.0.0/local_tts/_web/index.html +15 -0
- local_tts_studio-1.0.0/local_tts/cli.py +83 -0
- local_tts_studio-1.0.0/local_tts/py.typed +0 -0
- local_tts_studio-1.0.0/local_tts/server/__init__.py +0 -0
- local_tts_studio-1.0.0/local_tts/server/api.py +465 -0
- local_tts_studio-1.0.0/local_tts/server/app.py +199 -0
- local_tts_studio-1.0.0/local_tts/server/config.py +212 -0
- local_tts_studio-1.0.0/local_tts/server/logstore.py +133 -0
- local_tts_studio-1.0.0/local_tts/server/openai_api.py +375 -0
- local_tts_studio-1.0.0/local_tts/server/piper.py +295 -0
- local_tts_studio-1.0.0/local_tts/server/system.py +91 -0
- local_tts_studio-1.0.0/local_tts/server/voices.py +420 -0
- local_tts_studio-1.0.0/pyproject.toml +46 -0
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# OS
|
|
2
|
+
.DS_Store
|
|
3
|
+
Thumbs.db
|
|
4
|
+
|
|
5
|
+
# IDE
|
|
6
|
+
.idea/
|
|
7
|
+
.vscode/
|
|
8
|
+
*.swp
|
|
9
|
+
*.swo
|
|
10
|
+
|
|
11
|
+
# Environment
|
|
12
|
+
.env.local
|
|
13
|
+
.env.*.local
|
|
14
|
+
|
|
15
|
+
# Node
|
|
16
|
+
node_modules/
|
|
17
|
+
dist/
|
|
18
|
+
|
|
19
|
+
# Python
|
|
20
|
+
__pycache__/
|
|
21
|
+
*.pyc
|
|
22
|
+
*.pyo
|
|
23
|
+
*.egg-info/
|
|
24
|
+
.eggs/
|
|
25
|
+
*.egg
|
|
26
|
+
.venv/
|
|
27
|
+
venv/
|
|
28
|
+
.env/
|
|
29
|
+
|
|
30
|
+
# Logs
|
|
31
|
+
*.log
|
|
32
|
+
logs/
|
|
33
|
+
|
|
34
|
+
# Voice models & download cache (large binaries)
|
|
35
|
+
packages/local-tts/backend/data/
|
|
36
|
+
packages/local-tts/.local-tts-*.log
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 mythgram
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: local-tts-studio
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Local Piper text-to-speech studio: single-port web UI + REST + OpenAI-compatible speech API
|
|
5
|
+
Project-URL: Homepage, https://github.com/MythGrams/mythgram-agent/tree/main/packages/local-tts
|
|
6
|
+
Project-URL: Issues, https://github.com/MythGrams/mythgram-agent/issues
|
|
7
|
+
Author: mythgram
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: audio,local,openai,piper,speech,tts,voice
|
|
11
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
12
|
+
Classifier: Environment :: Web Environment
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Intended Audience :: End Users/Desktop
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Topic :: Internet :: WWW/HTTP :: HTTP Servers
|
|
22
|
+
Classifier: Topic :: Multimedia :: Sound/Audio :: Speech
|
|
23
|
+
Requires-Python: >=3.10
|
|
24
|
+
Requires-Dist: fastapi>=0.115
|
|
25
|
+
Requires-Dist: psutil>=6.1
|
|
26
|
+
Requires-Dist: uvicorn>=0.34
|
|
27
|
+
Provides-Extra: cuda
|
|
28
|
+
Requires-Dist: onnxruntime-gpu; extra == 'cuda'
|
|
29
|
+
Description-Content-Type: text/markdown
|
|
30
|
+
|
|
31
|
+
# local-tts-studio
|
|
32
|
+
|
|
33
|
+
Local Piper **text-to-speech** studio, shipped as a single pip package. One
|
|
34
|
+
command starts a server that opens **exactly one port (7878)** and serves:
|
|
35
|
+
|
|
36
|
+
- **Web UI** — speech studio, voice catalog/downloads, logs, system monitor
|
|
37
|
+
(`http://127.0.0.1:7878/`)
|
|
38
|
+
- **REST API** under `/api/*` (used by the web UI itself)
|
|
39
|
+
- **OpenAI-compatible speech API** under `/v1/*` (`/v1/audio/speech`) so any
|
|
40
|
+
OpenAI client can point at `http://127.0.0.1:7878/v1`
|
|
41
|
+
|
|
42
|
+
> PyPI name note: `local-tts` was blocked by PyPI's name-similarity policy, so
|
|
43
|
+
> the package is published as `local-tts-studio`. The command stays `local-tts`.
|
|
44
|
+
|
|
45
|
+
## Install
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
pip install local-tts-studio
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Run
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
local-tts
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Then open **http://127.0.0.1:7878** in your browser. The web UI, REST API and
|
|
58
|
+
OpenAI-compatible API are all served from that single port — no other ports are
|
|
59
|
+
opened. Press `Ctrl+C` to stop.
|
|
60
|
+
|
|
61
|
+
### Options
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
local-tts --host 0.0.0.0 # listen on the LAN (default: 127.0.0.1)
|
|
65
|
+
local-tts --port 7878 # change the port (default: 7878)
|
|
66
|
+
local-tts --version
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Environment overrides: `LOCAL_TTS_HOST`, `LOCAL_TTS_PORT`, `LOCAL_TTS_DATA_DIR`.
|
|
70
|
+
|
|
71
|
+
## First-time setup
|
|
72
|
+
|
|
73
|
+
1. Open the web UI → **Voices & setup**.
|
|
74
|
+
2. Click **Install piper** (installs the `piper-tts` engine into the same
|
|
75
|
+
Python environment, done once).
|
|
76
|
+
3. Pick a voice from the catalog to download (e.g. `en_US-amy-medium`).
|
|
77
|
+
4. Go to **Speech studio**, type text, synthesize.
|
|
78
|
+
|
|
79
|
+
## Configuration / data locations
|
|
80
|
+
|
|
81
|
+
Settings, downloaded voices and the voice catalog live in a per-user data
|
|
82
|
+
directory (override with `LOCAL_TTS_DATA_DIR`):
|
|
83
|
+
|
|
84
|
+
| OS | Path |
|
|
85
|
+
| ------- | ----------------------------------------------------------- |
|
|
86
|
+
| Linux | `~/.local/share/local-tts` |
|
|
87
|
+
| macOS | `~/Library/Application Support/local-tts` |
|
|
88
|
+
| Windows | `%LOCALAPPDATA%\local-tts` |
|
|
89
|
+
|
|
90
|
+
## OpenAI-compatible API example
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
curl http://127.0.0.1:7878/v1/audio/speech \
|
|
94
|
+
-H "Content-Type: application/json" \
|
|
95
|
+
-d '{"model":"local-tts","voice":"en_US-amy-medium","input":"Hello world"}' \
|
|
96
|
+
--output hello.wav
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## License
|
|
100
|
+
|
|
101
|
+
MIT
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# local-tts-studio
|
|
2
|
+
|
|
3
|
+
Local Piper **text-to-speech** studio, shipped as a single pip package. One
|
|
4
|
+
command starts a server that opens **exactly one port (7878)** and serves:
|
|
5
|
+
|
|
6
|
+
- **Web UI** — speech studio, voice catalog/downloads, logs, system monitor
|
|
7
|
+
(`http://127.0.0.1:7878/`)
|
|
8
|
+
- **REST API** under `/api/*` (used by the web UI itself)
|
|
9
|
+
- **OpenAI-compatible speech API** under `/v1/*` (`/v1/audio/speech`) so any
|
|
10
|
+
OpenAI client can point at `http://127.0.0.1:7878/v1`
|
|
11
|
+
|
|
12
|
+
> PyPI name note: `local-tts` was blocked by PyPI's name-similarity policy, so
|
|
13
|
+
> the package is published as `local-tts-studio`. The command stays `local-tts`.
|
|
14
|
+
|
|
15
|
+
## Install
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
pip install local-tts-studio
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Run
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
local-tts
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Then open **http://127.0.0.1:7878** in your browser. The web UI, REST API and
|
|
28
|
+
OpenAI-compatible API are all served from that single port — no other ports are
|
|
29
|
+
opened. Press `Ctrl+C` to stop.
|
|
30
|
+
|
|
31
|
+
### Options
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
local-tts --host 0.0.0.0 # listen on the LAN (default: 127.0.0.1)
|
|
35
|
+
local-tts --port 7878 # change the port (default: 7878)
|
|
36
|
+
local-tts --version
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Environment overrides: `LOCAL_TTS_HOST`, `LOCAL_TTS_PORT`, `LOCAL_TTS_DATA_DIR`.
|
|
40
|
+
|
|
41
|
+
## First-time setup
|
|
42
|
+
|
|
43
|
+
1. Open the web UI → **Voices & setup**.
|
|
44
|
+
2. Click **Install piper** (installs the `piper-tts` engine into the same
|
|
45
|
+
Python environment, done once).
|
|
46
|
+
3. Pick a voice from the catalog to download (e.g. `en_US-amy-medium`).
|
|
47
|
+
4. Go to **Speech studio**, type text, synthesize.
|
|
48
|
+
|
|
49
|
+
## Configuration / data locations
|
|
50
|
+
|
|
51
|
+
Settings, downloaded voices and the voice catalog live in a per-user data
|
|
52
|
+
directory (override with `LOCAL_TTS_DATA_DIR`):
|
|
53
|
+
|
|
54
|
+
| OS | Path |
|
|
55
|
+
| ------- | ----------------------------------------------------------- |
|
|
56
|
+
| Linux | `~/.local/share/local-tts` |
|
|
57
|
+
| macOS | `~/Library/Application Support/local-tts` |
|
|
58
|
+
| Windows | `%LOCALAPPDATA%\local-tts` |
|
|
59
|
+
|
|
60
|
+
## OpenAI-compatible API example
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
curl http://127.0.0.1:7878/v1/audio/speech \
|
|
64
|
+
-H "Content-Type: application/json" \
|
|
65
|
+
-d '{"model":"local-tts","voice":"en_US-amy-medium","input":"Hello world"}' \
|
|
66
|
+
--output hello.wav
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## License
|
|
70
|
+
|
|
71
|
+
MIT
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""local-tts: local Piper speech studio — web UI + REST + OpenAI-compatible API."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
6
|
+
|
|
7
|
+
for _dist in ("local-tts-studio", "local-tts"):
|
|
8
|
+
try:
|
|
9
|
+
__version__ = version(_dist)
|
|
10
|
+
break
|
|
11
|
+
except PackageNotFoundError:
|
|
12
|
+
__version__ = "0.0.0"
|
|
13
|
+
|
|
14
|
+
__all__ = ["__version__"]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
:root{--bg:#0d0f12;--raised:#14171c;--panel:#161a20;--panel-2:#1b2027;--inset:#10131a;--border:#262c36;--border-strong:#374050;--text:#e7eaf0;--text-dim:#9aa3b2;--text-faint:#67718a;--text-mute:#6d7686;--accent:#35c39b;--accent-strong:#52dcb4;--accent-press:#2aa78a;--accent-ink:#06231a;--accent-soft:#35c39b21;--warn:#d9a03a;--warn-soft:#d9a03a24;--danger:#e0564a;--danger-soft:#e0564a24;--info:#58a2e8;--info-soft:#58a2e821;--mono:ui-monospace, "SF Mono", "Cascadia Code", "JetBrains Mono", Menlo, Consolas, monospace;--sans:-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;--r-sm:6px;--r-md:8px;--r-lg:10px;--sidebar-w:236px}*{box-sizing:border-box}html,body,#root{height:100%}body{background:var(--bg);color:var(--text);font-family:var(--sans);-webkit-font-smoothing:antialiased;text-rendering:optimizelegibility;margin:0;font-size:14px;line-height:1.5}::selection{background:#35c39b47}:focus-visible{outline:2px solid var(--accent);outline-offset:1px;border-radius:2px}.shell{height:100%;display:flex}.sidebar{width:var(--sidebar-w);min-width:var(--sidebar-w);background:var(--raised);border-right:1px solid var(--border);flex-direction:column;gap:4px;padding:14px 12px;display:flex}.brand{align-items:center;gap:10px;padding:6px 8px 18px;display:flex}.brand-mark{background:var(--panel-2);border:1px solid var(--border-strong);width:30px;height:30px;color:var(--accent);border-radius:8px;place-items:center;display:grid}.brand-name{letter-spacing:.1px;font-size:14px;font-weight:650}.brand-sub{color:var(--text-dim);letter-spacing:.3px;font-size:11px}.nav-label{text-transform:uppercase;letter-spacing:1.2px;color:var(--text-mute);padding:10px 10px 6px;font-size:10px;font-weight:700}.nav-item{border-radius:var(--r-md);color:var(--text-dim);cursor:pointer;border:1px solid #0000;align-items:center;gap:10px;padding:8px 10px;font-size:13.5px;text-decoration:none;display:flex}.nav-item:hover{background:var(--panel);color:var(--text)}.nav-item.active{background:var(--panel-2);border-color:var(--border);color:var(--text)}.nav-item.active .nav-ico{color:var(--accent)}.nav-ico{color:var(--text-mute);place-items:center;display:grid}.sidebar-foot{margin-top:auto;padding:0 8px}.backend-chip{border:1px solid var(--border);background:var(--panel);border-radius:var(--r-md);color:var(--text-dim);align-items:center;gap:8px;padding:8px 10px;font-size:12px;display:flex}.backend-chip .dot{background:var(--danger);border-radius:50%;flex:none;width:7px;height:7px}.backend-chip.ok .dot{background:var(--accent)}.backend-chip .meta{font-family:var(--mono);color:var(--text-mute);margin-left:auto;font-size:10.5px}.content{flex:1;min-width:0;overflow-y:auto}.content-inner{max-width:1120px;margin:0 auto;padding:30px 34px 60px}.page-head{margin-bottom:22px}.page-head h1{letter-spacing:.1px;margin:0;font-size:19px;font-weight:650}.page-head p{color:var(--text-dim);max-width:640px;margin:4px 0 0;font-size:13px}.page-head-row{align-items:flex-start;gap:16px;display:flex}.page-head-row>:last-child{margin-left:auto}.stack{flex-direction:column;display:flex}.gap-8{gap:8px}.gap-12{gap:12px}.gap-16{gap:16px}.gap-24{gap:24px}.row{align-items:center;gap:10px;display:flex}.row-wrap{flex-wrap:wrap;align-items:center;gap:10px;display:flex}.spread{align-items:center;gap:10px;display:flex}.spread>:last-child{margin-left:auto}.grid-2{grid-template-columns:1fr 1fr;gap:14px;display:grid}@media (width<=860px){.grid-2{grid-template-columns:1fr}.content-inner{padding:22px 18px 60px}.shell{flex-direction:column}.sidebar{border-right:0;border-bottom:1px solid var(--border);flex-direction:row;align-items:center;width:100%;min-width:0;padding:10px 12px}.brand{padding:0}.brand-sub,.nav-label,.sidebar-foot{display:none}.sidebar{gap:0;margin-left:0}.nav-item{flex:none}.studio-cols{display:block!important}}.panel{background:var(--panel);border:1px solid var(--border);border-radius:var(--r-lg);overflow:hidden}.panel-head{border-bottom:1px solid var(--border);background:var(--panel-2);align-items:center;gap:10px;padding:12px 16px;display:flex}.panel-title{text-transform:uppercase;letter-spacing:1.1px;color:var(--text-dim);font-size:12px;font-weight:700}.panel-body{padding:16px}.panel-body.tight{padding:10px 12px}label.field{display:block}.field-name{text-transform:uppercase;letter-spacing:1.2px;color:var(--text-mute);align-items:baseline;gap:6px;margin-bottom:6px;font-size:11px;font-weight:700;display:flex}.field-hint{color:var(--text-mute);margin-top:6px;font-size:11.5px}.field-hint code{font-family:var(--mono);color:var(--text-dim);font-size:10.5px}input[type=text],input[type=search],textarea,select{background:var(--inset);width:100%;color:var(--text);border:1px solid var(--border);border-radius:var(--r-md);font:inherit;padding:8px 11px;font-size:13.5px;transition:border-color .12s}input::placeholder,textarea::placeholder{color:var(--text-faint)}input:hover,textarea:hover{border-color:var(--border-strong)}input:focus,textarea:focus,select:focus{border-color:var(--accent-press);outline:none}select{appearance:none;background-image:linear-gradient(45deg, transparent 50%, var(--text-mute) 50%), linear-gradient(135deg, var(--text-mute) 50%, transparent 50%);background-position:calc(100% - 16px) 55%,calc(100% - 11px) 55%;background-repeat:no-repeat;background-size:5px 5px;padding-right:28px}textarea{resize:vertical;min-height:260px;font-size:14.5px;line-height:1.65}input[type=range]{appearance:none;cursor:pointer;background:0 0;width:100%;height:22px;margin:0}input[type=range]::-webkit-slider-runnable-track{background:var(--panel-2);border:1px solid var(--border);border-radius:99px;height:4px}input[type=range]::-webkit-slider-thumb{appearance:none;background:var(--text);border:2px solid var(--bg);border-radius:50%;width:14px;height:14px;margin-top:-6px}input[type=range]:hover::-webkit-slider-thumb{background:var(--accent-strong)}input[type=range]::-moz-range-track{background:var(--panel-2);border:1px solid var(--border);border-radius:99px;height:4px}input[type=range]::-moz-range-thumb{background:var(--text);border:2px solid var(--bg);border-radius:50%;width:10px;height:10px}.switch{cursor:pointer;color:var(--text-dim);-webkit-user-select:none;user-select:none;align-items:center;gap:8px;font-size:13px;display:inline-flex}.switch input{display:none}.switch .track{background:var(--panel-2);border:1px solid var(--border-strong);border-radius:99px;width:30px;height:17px;transition:background .15s;position:relative}.switch .track:after{content:"";background:var(--text-mute);border-radius:50%;width:11px;height:11px;transition:left .15s,background .15s;position:absolute;top:2px;left:2px}.switch input:checked+.track{background:var(--accent-press);border-color:var(--accent-press)}.switch input:checked+.track:after{background:#fff;left:15px}.btn{font:inherit;color:var(--text);background:var(--panel-2);border:1px solid var(--border-strong);border-radius:var(--r-md);cursor:pointer;white-space:nowrap;justify-content:center;align-items:center;gap:8px;padding:7px 14px;font-size:13px;font-weight:600;transition:background .12s,border-color .12s,transform 50ms;display:inline-flex}.btn:hover:not(:disabled){background:#232935;border-color:#465062}.btn:active:not(:disabled){transform:translateY(1px)}.btn:disabled{opacity:.45;cursor:not-allowed}.btn.primary{background:var(--accent);border-color:var(--accent);color:var(--accent-ink)}.btn.primary:hover:not(:disabled){background:var(--accent-strong);border-color:var(--accent-strong)}.btn.primary:active:not(:disabled){background:var(--accent-press)}.btn.danger{color:#f2857b;background:0 0;border-color:#e0564a66}.btn.danger:hover:not(:disabled){background:var(--danger-soft);border-color:var(--danger)}.btn.ghost{border-color:var(--border);color:var(--text-dim);background:0 0}.btn.ghost:hover:not(:disabled){color:var(--text);border-color:var(--border-strong);background:var(--panel-2)}.btn.sm{padding:4px 10px;font-size:12px}.btn .spin{animation:.9s linear infinite spin}@keyframes spin{to{transform:rotate(360deg)}}.icon-btn{border-radius:var(--r-sm);width:28px;height:28px;color:var(--text-mute);cursor:pointer;background:0 0;border:1px solid #0000;place-items:center;display:inline-grid}.icon-btn:hover{color:var(--text);background:var(--panel-2);border-color:var(--border)}.icon-btn.danger:hover{color:#f2857b;background:var(--danger-soft);border-color:#e0564a59}.pill{letter-spacing:.3px;border:1px solid var(--border-strong);color:var(--text-dim);background:var(--panel-2);font-size:11px;font-weight:600;font-family:var(--mono);border-radius:99px;align-items:center;gap:6px;padding:2px 9px;display:inline-flex}.pill .dot{background:var(--text-mute);border-radius:50%;width:6px;height:6px}.pill.ok{color:var(--accent-strong);background:var(--accent-soft);border-color:#35c39b59}.pill.ok .dot{background:var(--accent)}.pill.warn{color:var(--warn);background:var(--warn-soft);border-color:#d9a03a59}.pill.warn .dot{background:var(--warn)}.pill.danger{color:#f09186;background:var(--danger-soft);border-color:#e0564a66}.pill.danger .dot{background:var(--danger)}.pill.plain{background:0 0}.pill.big{padding:4px 11px;font-size:12px}.tag{text-transform:uppercase;letter-spacing:.8px;color:var(--text-mute);border:1px solid var(--border);background:var(--inset);border-radius:4px;padding:1px 7px;font-size:10.5px;font-weight:700}.tag.lua{color:var(--accent-strong);background:var(--accent-soft);border-color:#35c39b4d}.meter{background:var(--inset);border:1px solid var(--border);border-radius:3px;height:5px;position:relative;overflow:hidden}.meter>i{background:var(--accent);border-radius:3px;min-width:0;height:100%;transition:width .3s;display:block}.meter.indeterminate>i{animation:1.1s ease-in-out infinite indet;width:30%!important}@keyframes indet{0%{margin-left:-35%}to{margin-left:100%}}.stats{border-top:1px solid var(--border);grid-template-columns:repeat(auto-fit,minmax(230px,1fr));gap:0;display:grid}.stat{border-bottom:1px solid var(--border);border-right:1px solid var(--border);min-width:0;padding:12px 16px}.stats .stat:nth-child(2n){border-right:0}.stat-top{align-items:baseline;gap:8px;margin-bottom:6px;display:flex}.stat-name{text-transform:uppercase;letter-spacing:1px;color:var(--text-mute);font-size:10.5px;font-weight:700}.stat-value{font-family:var(--mono);color:var(--text);white-space:nowrap;margin-left:auto;font-size:12px}.stat-sub{color:var(--text-mute);font-size:11px;font-family:var(--mono);white-space:nowrap;text-overflow:ellipsis;margin-top:4px;overflow:hidden}.voice-row{border-bottom:1px solid var(--border);align-items:center;gap:12px;padding:9px 16px;transition:background .1s;display:flex}.voice-row:last-child{border-bottom:0}.voice-row:hover{background:#ffffff05}.voice-row.playable{cursor:pointer}.voice-row.playable:hover{background:var(--panel-2)}.voice-row.selected{background:var(--accent-soft);box-shadow:inset 2px 0 0 var(--accent)}.lang-flag{font-family:var(--mono);color:var(--text-mute);border:1px solid var(--border);background:var(--inset);text-align:center;border-radius:4px;min-width:44px;padding:2px 5px;font-size:10px}.voice-name{font-size:13.5px;font-weight:550}.voice-meta{color:var(--text-mute);margin-top:1px;font-size:11.5px}.filters-bar{border-bottom:1px solid var(--border);flex-wrap:wrap;align-items:center;gap:10px;padding:12px 16px;display:flex}.filters-bar select,.filters-bar input[type=search]{width:auto}.filters-bar input[type=search]{flex:1;min-width:180px}.group-head{letter-spacing:1px;text-transform:uppercase;color:var(--text-mute);background:var(--inset);border-bottom:1px solid var(--border);align-items:center;gap:8px;padding:7px 16px;font-size:10.5px;font-weight:700;display:flex}.banner{background:var(--warn-soft);color:#ecc27a;border-radius:var(--r-md);border:1px solid #d9a03a66;align-items:center;gap:10px;margin-bottom:14px;padding:10px 14px;font-size:13px;display:flex}.banner.error{background:var(--danger-soft);color:#f09186;border-color:#e0564a66}.banner.error a{color:#ffb0a5}.banner a{color:var(--accent-strong);font-weight:600}.banner .ico{flex:none;place-items:center;display:grid}.studio-cols{grid-template-columns:minmax(0,1fr) 320px;align-items:start;gap:18px;display:grid}.takes-list{flex-direction:column;gap:8px;display:flex}.take{border:1px solid var(--border);border-radius:var(--r-md);background:var(--panel-2);cursor:pointer;min-width:0;padding:9px 12px;transition:border-color .12s}.take:hover{border-color:var(--border-strong)}.take.on{border-color:var(--accent-press);background:var(--accent-soft)}.take-top{align-items:center;gap:8px;display:flex}.take-play{color:var(--text-dim);place-items:center;display:grid}.take.on .take-play{color:var(--accent)}.take-text{color:var(--text-dim);white-space:nowrap;text-overflow:ellipsis;margin-top:3px;font-size:12.5px;overflow:hidden}.take-meta{font-family:var(--mono);color:var(--text-mute);white-space:nowrap;margin-left:auto;font-size:10.5px}.param-row{padding:9px 0}.param-head{align-items:baseline;margin-bottom:2px;display:flex}.param-val{font-family:var(--mono);color:var(--text-dim);margin-left:auto;font-size:11.5px}.gen-meta{font-family:var(--mono);color:var(--text-mute);flex-wrap:wrap;gap:14px;font-size:11px;display:flex}.gen-meta b{color:var(--text-dim);font-weight:600}.result-empty{border:1px dashed var(--border-strong);border-radius:var(--r-md);text-align:center;color:var(--text-mute);background:var(--inset);padding:26px 18px;font-size:13px}.result-empty .ico{color:var(--text-mute);margin:0 auto 8px}audio.player{width:100%;height:40px}.log-list{font-family:var(--mono);height:calc(100vh - 280px);min-height:320px;font-size:12px;overflow-y:auto}.log-row{border-bottom:1px solid #ffffff06;align-items:baseline;gap:10px;padding:3px 16px;display:flex}.log-row:hover{background:#ffffff05}.log-time{color:var(--text-mute);flex:none;font-size:11px}.log-src{color:var(--text-dim);text-overflow:ellipsis;white-space:nowrap;flex:none;min-width:110px;overflow:hidden}.log-msg{color:var(--text);white-space:pre-wrap;word-break:break-word;min-width:0}.log-row.warning{background:#d9a03a0f}.log-row.warning .log-msg{color:#ecc27a}.log-row.error{background:#e0564a12}.log-row.error .log-msg{color:#f09186}.log-row.debug .log-time,.log-row.debug .log-src{opacity:.6}.log-row .lvl{text-align:center;letter-spacing:.5px;border-radius:3px;flex:none;align-self:center;width:44px;padding:1px 0;font-size:9.5px;font-weight:700}.log-row.debug .lvl{background:var(--panel-2);color:var(--text-mute);border:1px solid var(--border)}.log-row.info .lvl{background:var(--info-soft);color:var(--info);border:1px solid #58a2e84d}.log-row.warning .lvl{background:var(--warn-soft);color:var(--warn);border:1px solid #d9a03a4d}.log-row.error .lvl{background:var(--danger-soft);color:var(--danger);border:1px solid #e0564a4d}.log-live-dot{color:var(--text-mute);font-size:11px;font-family:var(--mono);align-items:center;gap:6px;display:inline-flex}.log-live-dot .dot{background:var(--danger);border-radius:50%;width:7px;height:7px}.log-live-dot.on .dot{background:var(--accent);animation:2s infinite pulse}@keyframes pulse{0%,to{opacity:1}50%{opacity:.35}}.empty-state{text-align:center;color:var(--text-mute);padding:34px 20px;font-size:13px}.empty-state .ico{color:var(--text-mute);place-items:center;margin:0 auto 10px;display:grid}.empty-state .hint{margin-top:6px;font-size:12px}.empty-state a{color:var(--accent-strong)}.api-url-row input{flex:1;min-width:0;font-size:12.5px!important}.pill .meta{opacity:.75;margin-left:4px;font-size:9px}.docs-section{margin-bottom:2px}.docs-hint{color:var(--text-dim);margin:4px 0 12px;font-size:13px}.docs-h3{text-transform:uppercase;letter-spacing:1.2px;color:var(--text-mute);margin:20px 0 6px;font-size:11px;font-weight:700}.docs-section ul{color:var(--text-dim);margin:8px 0 6px;padding-left:18px}.docs-section li{margin:5px 0;font-size:13px}.docs-section li b{color:var(--text)}.docs-section p{color:var(--text-dim);margin:8px 0;font-size:13px;line-height:1.6}.docs-section a{color:var(--accent-strong);text-decoration:none}.docs-section a:hover{text-decoration:underline}.chip-nav{flex-wrap:wrap;gap:6px;margin-bottom:18px;display:flex}.chip-nav a{font-family:var(--mono);color:var(--text-dim);border:1px solid var(--border);background:var(--panel);border-radius:99px;padding:3px 10px;font-size:11px;text-decoration:none}.chip-nav a:hover{color:var(--text);border-color:var(--border-strong);background:var(--panel-2)}.code-block{border:1px solid var(--border);border-radius:var(--r-md);background:var(--inset);margin:8px 0 12px;overflow:hidden}.code-block-head{background:var(--panel-2);border-bottom:1px solid var(--border);font-family:var(--mono);color:var(--text-mute);text-transform:uppercase;letter-spacing:1px;align-items:center;gap:8px;padding:5px 10px;font-size:10px;display:flex}.code-block pre{font-family:var(--mono);color:var(--text);white-space:pre;tab-size:2;margin:0;padding:11px 14px;font-size:12px;line-height:1.62;overflow-x:auto}.copy-btn{font:inherit;font-size:10.5px;font-family:var(--mono);color:var(--text-dim);border:1px solid var(--border);border-radius:var(--r-sm);cursor:pointer;white-space:nowrap;text-transform:none;letter-spacing:0;background:0 0;align-items:center;gap:5px;padding:2px 8px;display:inline-flex}.copy-btn:hover{color:var(--text);border-color:var(--border-strong);background:var(--panel)}.copy-btn:disabled{opacity:.4;border:1px solid var(--border-strong)}.docs-table{border-collapse:collapse;width:100%;margin:8px 0 12px;font-size:12.5px}.docs-table th,.docs-table td{border:1px solid var(--border);text-align:left;vertical-align:top;padding:7px 10px}.docs-table th{background:var(--panel-2);text-transform:uppercase;letter-spacing:1px;color:var(--text-mute);white-space:nowrap;font-size:10.5px;font-weight:700}.docs-table td{color:var(--text-dim);line-height:1.55}.docs-table tr:nth-child(2n) td{background:#ffffff03}.docs-table td code{font-family:var(--mono);color:var(--accent-strong);font-size:11px}.docs-table td code.inline{color:var(--accent-strong);background:0 0;border:0;padding:0}.dotline{color:var(--text-dim);align-items:center;gap:6px;font-size:12px;display:flex}.dot{border-radius:50%;flex:none;width:7px;height:7px}.dot.ok{background:var(--accent)}.dot.off{background:var(--text-mute)}.dot.warn{background:var(--warn)}code.inline{font-family:var(--mono);background:var(--inset);border:1px solid var(--border);color:var(--text-dim);border-radius:4px;padding:1px 6px;font-size:11.5px}.mono{font-family:var(--mono)}.dim{color:var(--text-dim)}.mute{color:var(--text-mute)}.small{font-size:12px}.hr{border:0;border-top:1px solid var(--border);margin:14px 0}::-webkit-scrollbar{width:10px;height:10px}::-webkit-scrollbar-thumb{border:2px solid var(--bg);background:#2a303b;border-radius:8px}::-webkit-scrollbar-thumb:hover{background:#39414f}::-webkit-scrollbar-track{background:0 0}
|